Implement a simple config file parser
This commit is contained in:
parent
405b68b824
commit
167b8bee8d
20 changed files with 601 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
||||||
# ---> C++
|
# ---> C++
|
||||||
|
# LSP Cache
|
||||||
|
.cache/
|
||||||
|
|
||||||
# Build dirs
|
# Build dirs
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
|
314
Garbage/Include/Garbage/SimpleConf.hpp
Normal file
314
Garbage/Include/Garbage/SimpleConf.hpp
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
#ifndef GARBAGE_SIMPLE_CONF_HPP
|
||||||
|
#define GARBAGE_SIMPLE_CONF_HPP
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Garbage
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
enum class SimpleConfErrorKind
|
||||||
|
{
|
||||||
|
ReadError,
|
||||||
|
ConversionError,
|
||||||
|
ParseError,
|
||||||
|
LookupError,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr
|
||||||
|
std::string AsString(SimpleConfErrorKind err)
|
||||||
|
{
|
||||||
|
switch (err) {
|
||||||
|
case SimpleConfErrorKind::ReadError:
|
||||||
|
return "Read Error";
|
||||||
|
case SimpleConfErrorKind::ConversionError:
|
||||||
|
return "Conversion Error";
|
||||||
|
case SimpleConfErrorKind::ParseError:
|
||||||
|
return "Parse Error";
|
||||||
|
case SimpleConfErrorKind::LookupError:
|
||||||
|
return "Lookup Error";
|
||||||
|
default:
|
||||||
|
return "Unknown Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleConfError : public std::runtime_error
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SimpleConfError(SimpleConfErrorKind kind, std::string_view message)
|
||||||
|
: std::runtime_error(AsString(kind) + ": " + std::string(message))
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace SimpleConfImplementation
|
||||||
|
{
|
||||||
|
class ReadError : public SimpleConfError
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ReadError(std::string_view message)
|
||||||
|
: SimpleConfError(SimpleConfErrorKind::ReadError, message)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConversionError : public SimpleConfError
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConversionError(std::string_view message)
|
||||||
|
: SimpleConfError(SimpleConfErrorKind::ConversionError, message)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ParseError : public SimpleConfError
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ParseError(std::size_t line, std::string_view message)
|
||||||
|
: SimpleConfError(SimpleConfErrorKind::ParseError,
|
||||||
|
std::format("[Line {}] {}", line, message))
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LookupError : public SimpleConfError
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LookupError(std::string_view message)
|
||||||
|
: SimpleConfError(SimpleConfErrorKind::LookupError, message)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr
|
||||||
|
T Convert(std::string_view rawValue)
|
||||||
|
{
|
||||||
|
if constexpr (std::is_same_v<T, std::string>) {
|
||||||
|
return std::string(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) {
|
||||||
|
T result{};
|
||||||
|
auto [ptr, ec] = std::from_chars(rawValue.data(),
|
||||||
|
rawValue.data() + rawValue.size(),
|
||||||
|
result);
|
||||||
|
|
||||||
|
if (ptr != rawValue.data() + rawValue.size()) {
|
||||||
|
throw ConversionError("Could not parse the whole value");
|
||||||
|
}
|
||||||
|
else if (ec == std::errc::invalid_argument) {
|
||||||
|
throw ConversionError("The value is not a valid number");
|
||||||
|
}
|
||||||
|
else if (ec == std::errc::result_out_of_range) {
|
||||||
|
throw ConversionError("The value is too large to store in the requested type");
|
||||||
|
}
|
||||||
|
else if (ec != std::errc()) {
|
||||||
|
throw ConversionError("Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ConversionError("Can't convert the raw value to the requested type");
|
||||||
|
}
|
||||||
|
|
||||||
|
using PairsType = std::unordered_map<std::string_view, std::string_view>;
|
||||||
|
|
||||||
|
constexpr
|
||||||
|
void Parse(const std::string& rawConf, PairsType& pairs)
|
||||||
|
{
|
||||||
|
std::size_t current = 0;
|
||||||
|
std::size_t currentLine = 1;
|
||||||
|
|
||||||
|
enum class State
|
||||||
|
{
|
||||||
|
Initial,
|
||||||
|
ReadingKey,
|
||||||
|
ReadingValue,
|
||||||
|
CommentLine,
|
||||||
|
} currentState = State::Initial;
|
||||||
|
|
||||||
|
std::size_t keyBegin = 0;
|
||||||
|
std::size_t keyEnd = 0;
|
||||||
|
std::size_t valueBegin = 0;
|
||||||
|
std::size_t valueEnd = 0;
|
||||||
|
|
||||||
|
auto addPair = [&] {
|
||||||
|
if (keyBegin == keyEnd) {
|
||||||
|
throw ParseError(currentLine, "Empty key is not allowed");
|
||||||
|
}
|
||||||
|
// Allow empty values?
|
||||||
|
|
||||||
|
std::string_view key(rawConf.data() + keyBegin, keyEnd - keyBegin);
|
||||||
|
std::string_view value(rawConf.data() + valueBegin, valueEnd - valueBegin);
|
||||||
|
if (value.size() == 0) {
|
||||||
|
value = std::string_view(nullptr, 0);
|
||||||
|
}
|
||||||
|
pairs[key] = value;
|
||||||
|
|
||||||
|
keyBegin = 0;
|
||||||
|
keyEnd = 0;
|
||||||
|
valueBegin = 0;
|
||||||
|
valueEnd = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (current < rawConf.size()) {
|
||||||
|
switch (currentState) {
|
||||||
|
break; case State::Initial:
|
||||||
|
if (std::isspace(rawConf[current])) {
|
||||||
|
if (rawConf[current] == '\n') {
|
||||||
|
++currentLine;
|
||||||
|
}
|
||||||
|
++current;
|
||||||
|
}
|
||||||
|
else if (rawConf[current] == '#') {
|
||||||
|
currentState = State::CommentLine;
|
||||||
|
}
|
||||||
|
else if (rawConf[current] == '=') {
|
||||||
|
throw ParseError(currentLine, "Empty key is not allowed");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
keyBegin = current;
|
||||||
|
keyEnd = current + 1; // We know there is at least one char
|
||||||
|
currentState = State::ReadingKey;
|
||||||
|
}
|
||||||
|
break; case State::ReadingKey:
|
||||||
|
if (rawConf[current] == '=') {
|
||||||
|
currentState = State::ReadingValue;
|
||||||
|
}
|
||||||
|
else if (rawConf[current] == '#') {
|
||||||
|
throw ParseError(currentLine, "Unexpected comment start");
|
||||||
|
}
|
||||||
|
else if (!std::isspace(rawConf[current])) {
|
||||||
|
keyEnd = current + 1;
|
||||||
|
}
|
||||||
|
else if (rawConf[current] == '\n') {
|
||||||
|
throw ParseError(currentLine, "End of line while reading key");
|
||||||
|
}
|
||||||
|
++current;
|
||||||
|
break; case State::ReadingValue:
|
||||||
|
if (rawConf[current] == '\n') {
|
||||||
|
addPair();
|
||||||
|
|
||||||
|
++currentLine;
|
||||||
|
currentState = State::Initial;
|
||||||
|
}
|
||||||
|
else if (rawConf[current] == '=') {
|
||||||
|
throw ParseError(currentLine, "Multiple key/value separators are illegal");
|
||||||
|
}
|
||||||
|
else if (rawConf[current] == '#') {
|
||||||
|
addPair();
|
||||||
|
currentState = State::CommentLine;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!std::isspace(rawConf[current])) {
|
||||||
|
if (valueBegin == 0) {
|
||||||
|
valueBegin = current;
|
||||||
|
valueEnd = current + 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
valueEnd = current + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++current;
|
||||||
|
break; case State::CommentLine:
|
||||||
|
if (rawConf[current] == '\n') {
|
||||||
|
++currentLine;
|
||||||
|
currentState = State::Initial;
|
||||||
|
}
|
||||||
|
++current;
|
||||||
|
break; default:
|
||||||
|
throw ParseError(currentLine, "Invalid parser state (should be impossible)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState == State::ReadingKey) {
|
||||||
|
throw ParseError(currentLine, "End of config while reading the key");
|
||||||
|
} else if (currentState == State::ReadingValue) {
|
||||||
|
addPair();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleConf
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Required {};
|
||||||
|
|
||||||
|
SimpleConf(const fs::path& path)
|
||||||
|
{
|
||||||
|
using namespace SimpleConfImplementation;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
if (!fs::exists(path, ec) || ec) {
|
||||||
|
throw ReadError("The specified file does not exist");
|
||||||
|
}
|
||||||
|
if (!fs::is_regular_file(path, ec) || ec) {
|
||||||
|
throw ReadError("The the specified path is not a regular file");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t fileSize = fs::file_size(path, ec);
|
||||||
|
if (ec) {
|
||||||
|
throw ReadError("Could not read the file size");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
throw ReadError("Could not open the file for reading");
|
||||||
|
}
|
||||||
|
|
||||||
|
mConfContent = std::string(fileSize, '\0');
|
||||||
|
file.read(mConfContent.data(), fileSize);
|
||||||
|
|
||||||
|
Parse(mConfContent, mPairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleConf(std::string&& strConfig)
|
||||||
|
: mConfContent(std::move(strConfig))
|
||||||
|
{
|
||||||
|
using namespace SimpleConfImplementation;
|
||||||
|
|
||||||
|
Parse(mConfContent, mPairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::optional<T> GetOptional(std::string_view key)
|
||||||
|
{
|
||||||
|
using namespace SimpleConfImplementation;
|
||||||
|
|
||||||
|
if (auto it = mPairs.find(key); it != mPairs.end()) {
|
||||||
|
return Convert<T>(it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T, typename RequireValue = void>
|
||||||
|
T Get(std::string_view key, const T& defaultValue = {})
|
||||||
|
{
|
||||||
|
using namespace SimpleConfImplementation;
|
||||||
|
|
||||||
|
if (auto it = mPairs.find(key); it != mPairs.end()) {
|
||||||
|
return Convert<T>(it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<RequireValue, Required>) {
|
||||||
|
throw LookupError("Required config key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
std::string mConfContent;
|
||||||
|
SimpleConfImplementation::PairsType mPairs;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // GARBAGE_SIMPLE_CONF_HPP
|
|
@ -2,3 +2,4 @@ include(${PROJECT_SOURCE_DIR}/CMake/Catch2.cmake)
|
||||||
|
|
||||||
add_subdirectory(Base64)
|
add_subdirectory(Base64)
|
||||||
add_subdirectory(SQLite)
|
add_subdirectory(SQLite)
|
||||||
|
add_subdirectory(SimpleConf)
|
||||||
|
|
29
Tests/SimpleConf/CMakeLists.txt
Normal file
29
Tests/SimpleConf/CMakeLists.txt
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
add_executable(TestSimpleConf)
|
||||||
|
|
||||||
|
target_compile_features(TestSimpleConf
|
||||||
|
PRIVATE
|
||||||
|
cxx_std_20
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(TestSimpleConf
|
||||||
|
PRIVATE
|
||||||
|
Garbage
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
|
||||||
|
target_sources(TestSimpleConf
|
||||||
|
PRIVATE
|
||||||
|
Comments.cpp
|
||||||
|
Conversions.cpp
|
||||||
|
EmptyConfigs.cpp
|
||||||
|
Optionality.cpp
|
||||||
|
Parsing.cpp
|
||||||
|
WhitespaceBehaviour.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET TestSimpleConf POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TestConfigs
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/TestConfigs
|
||||||
|
)
|
15
Tests/SimpleConf/Comments.cpp
Normal file
15
Tests/SimpleConf/Comments.cpp
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <Garbage/SimpleConf.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Comments and empty lines are ignored")
|
||||||
|
{
|
||||||
|
using Required = Garbage::SimpleConf::Required;
|
||||||
|
using LookupError = Garbage::SimpleConfImplementation::LookupError;
|
||||||
|
|
||||||
|
std::filesystem::path path("TestConfigs/Comments.conf");
|
||||||
|
|
||||||
|
Garbage::SimpleConf config(path);
|
||||||
|
|
||||||
|
REQUIRE_THROWS_AS((config.Get<std::string, Required>("this shouldn't")), LookupError);
|
||||||
|
REQUIRE(config.Get<std::string, Required>("while this") == "is accessible");
|
||||||
|
}
|
41
Tests/SimpleConf/Conversions.cpp
Normal file
41
Tests/SimpleConf/Conversions.cpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <Garbage/SimpleConf.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Check if type conversions work correctly")
|
||||||
|
{
|
||||||
|
using Required = Garbage::SimpleConf::Required;
|
||||||
|
|
||||||
|
std::filesystem::path path("TestConfigs/BasicConversions.conf");
|
||||||
|
|
||||||
|
Garbage::SimpleConf config(path);
|
||||||
|
|
||||||
|
SECTION("Basic stuff that should be correct")
|
||||||
|
{
|
||||||
|
REQUIRE(config.Get<std::string, Required>("an int") == "123456");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("a float") == "3.5");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("a long (8B)") == "121212121212121212");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("a double") == "0.0000128");
|
||||||
|
|
||||||
|
REQUIRE(config.Get<std::int32_t>("an int") == 123456);
|
||||||
|
REQUIRE(config.Get<float>("a float") == 3.5f);
|
||||||
|
REQUIRE(config.Get<std::int64_t>("a long (8B)") == 121212121212121212);
|
||||||
|
REQUIRE(config.Get<double>("a double") == 0.0000128);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Stuff that should fail to parse")
|
||||||
|
{
|
||||||
|
REQUIRE(config.Get<std::string, Required>("an incorrect char") == "256");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("unsigned") == "-34");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("signed, but large") == "3000000000");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("mangled float") == "3.f4");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("mangled float 2") == "2.4f");
|
||||||
|
|
||||||
|
using ConversionError = Garbage::SimpleConfImplementation::ConversionError;
|
||||||
|
|
||||||
|
REQUIRE_THROWS_AS(config.Get<std::uint8_t>("an incorrect char"), ConversionError);
|
||||||
|
REQUIRE_THROWS_AS(config.Get<std::uint32_t>("unsigned"), ConversionError);
|
||||||
|
REQUIRE_THROWS_AS(config.Get<std::int32_t>("signed, but large"), ConversionError);
|
||||||
|
REQUIRE_THROWS_AS(config.Get<float>("mangled float"), ConversionError);
|
||||||
|
REQUIRE_THROWS_AS(config.Get<float>("mangled float 2"), ConversionError);
|
||||||
|
}
|
||||||
|
}
|
17
Tests/SimpleConf/EmptyConfigs.cpp
Normal file
17
Tests/SimpleConf/EmptyConfigs.cpp
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <Garbage/SimpleConf.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Config files with no key value pairs")
|
||||||
|
{
|
||||||
|
SECTION("Empty file")
|
||||||
|
{
|
||||||
|
std::filesystem::path path("TestConfigs/Empty.conf");
|
||||||
|
REQUIRE_NOTHROW(Garbage::SimpleConf(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("File with comments")
|
||||||
|
{
|
||||||
|
std::filesystem::path path("TestConfigs/CommentsOnly.conf");
|
||||||
|
REQUIRE_NOTHROW(Garbage::SimpleConf(path));
|
||||||
|
}
|
||||||
|
}
|
25
Tests/SimpleConf/Optionality.cpp
Normal file
25
Tests/SimpleConf/Optionality.cpp
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <Garbage/SimpleConf.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Check handling of optional values")
|
||||||
|
{
|
||||||
|
std::filesystem::path path("TestConfigs/Optionality.conf");
|
||||||
|
|
||||||
|
Garbage::SimpleConf config(path);
|
||||||
|
|
||||||
|
SECTION("GetOptional behaviour")
|
||||||
|
{
|
||||||
|
REQUIRE(!config.GetOptional<std::string>("non-existent key"));
|
||||||
|
REQUIRE(*config.GetOptional<std::string>("existing key") == "existing value");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Get behaviour")
|
||||||
|
{
|
||||||
|
using Required = Garbage::SimpleConf::Required;
|
||||||
|
using LookupError = Garbage::SimpleConfImplementation::LookupError;
|
||||||
|
|
||||||
|
REQUIRE(config.Get<std::string>("non-existent key", "a default") == "a default");
|
||||||
|
REQUIRE(config.Get<std::string>("existing key") == "existing value");
|
||||||
|
REQUIRE_THROWS_AS((config.Get<std::string, Required>("non-existent key")), LookupError);
|
||||||
|
}
|
||||||
|
}
|
40
Tests/SimpleConf/Parsing.cpp
Normal file
40
Tests/SimpleConf/Parsing.cpp
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||||
|
#include <Garbage/SimpleConf.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Ensure robust parsing")
|
||||||
|
{
|
||||||
|
std::filesystem::path configs("TestConfigs");
|
||||||
|
|
||||||
|
using Catch::Matchers::ContainsSubstring;
|
||||||
|
|
||||||
|
SECTION("Multiple assignments")
|
||||||
|
{
|
||||||
|
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-MultipleEquals.conf"),
|
||||||
|
ContainsSubstring("[Line 3]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Multiple assignments")
|
||||||
|
{
|
||||||
|
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-HashInKey.conf"),
|
||||||
|
ContainsSubstring("[Line 5]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("No equals")
|
||||||
|
{
|
||||||
|
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-OnlyKey.conf"),
|
||||||
|
ContainsSubstring("[Line 7]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Empty key")
|
||||||
|
{
|
||||||
|
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-EmptyKey.conf"),
|
||||||
|
ContainsSubstring("[Line 7]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Key at end of file")
|
||||||
|
{
|
||||||
|
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-KeyAtEnd.conf"),
|
||||||
|
ContainsSubstring("[Line 10]"));
|
||||||
|
}
|
||||||
|
}
|
12
Tests/SimpleConf/TestConfigs/BasicConversions.conf
Normal file
12
Tests/SimpleConf/TestConfigs/BasicConversions.conf
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# First, we wanna test if it can even do the basics
|
||||||
|
an int = 123456
|
||||||
|
a float = 3.5
|
||||||
|
a long (8B) = 121212121212121212
|
||||||
|
a double = 0.0000128
|
||||||
|
|
||||||
|
# Now we want to see if it correctly blows up
|
||||||
|
an incorrect char = 256
|
||||||
|
unsigned = -34
|
||||||
|
signed, but large = 3000000000
|
||||||
|
mangled float = 3.f4
|
||||||
|
mangled float 2 = 2.4f
|
4
Tests/SimpleConf/TestConfigs/Comments.conf
Normal file
4
Tests/SimpleConf/TestConfigs/Comments.conf
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Test whether comments behave correctly
|
||||||
|
|
||||||
|
# this shouldn't = be accessible
|
||||||
|
while this = is accessible # and this comment doesn't interfere
|
3
Tests/SimpleConf/TestConfigs/CommentsOnly.conf
Normal file
3
Tests/SimpleConf/TestConfigs/CommentsOnly.conf
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Another example of an empty config
|
||||||
|
# The config should initialize successfully,
|
||||||
|
# but there should be no kv-pairs inside it
|
0
Tests/SimpleConf/TestConfigs/Empty.conf
Normal file
0
Tests/SimpleConf/TestConfigs/Empty.conf
Normal file
2
Tests/SimpleConf/TestConfigs/Optionality.conf
Normal file
2
Tests/SimpleConf/TestConfigs/Optionality.conf
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# This probably doesn't need to exist, but eh
|
||||||
|
existing key = existing value
|
10
Tests/SimpleConf/TestConfigs/Parsing-EmptyKey.conf
Normal file
10
Tests/SimpleConf/TestConfigs/Parsing-EmptyKey.conf
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# A simple config file that
|
||||||
|
|
||||||
|
normal_key=normal_value
|
||||||
|
another_key=another_value
|
||||||
|
generic=generic
|
||||||
|
another_one=another_value
|
||||||
|
= oh no, an empty key
|
||||||
|
another_one=...
|
||||||
|
|
||||||
|
# It doesn't really matter what's here, because the parser won't reach it
|
8
Tests/SimpleConf/TestConfigs/Parsing-HashInKey.conf
Normal file
8
Tests/SimpleConf/TestConfigs/Parsing-HashInKey.conf
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# A simple config file that contains a # character inside the key
|
||||||
|
|
||||||
|
normal_key=normal_value
|
||||||
|
another_key=another_value
|
||||||
|
a_very_bas#ic_key=generic_value
|
||||||
|
another_one=...
|
||||||
|
|
||||||
|
# It doesn't really matter what's here, because the parser won't reach it
|
10
Tests/SimpleConf/TestConfigs/Parsing-KeyAtEnd.conf
Normal file
10
Tests/SimpleConf/TestConfigs/Parsing-KeyAtEnd.conf
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# A simple config file that
|
||||||
|
|
||||||
|
normal_key=normal_value
|
||||||
|
another_key=another_value
|
||||||
|
generic=generic
|
||||||
|
another_one=another_value
|
||||||
|
another_one=...
|
||||||
|
|
||||||
|
# It doesn't really matter what's here, because the parser won't reach it
|
||||||
|
sneaky_key
|
4
Tests/SimpleConf/TestConfigs/Parsing-MultipleEquals.conf
Normal file
4
Tests/SimpleConf/TestConfigs/Parsing-MultipleEquals.conf
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# A simple config
|
||||||
|
that appears = correct
|
||||||
|
but it = has = an invalid line # Should blow up here
|
||||||
|
another=pair
|
10
Tests/SimpleConf/TestConfigs/Parsing-OnlyKey.conf
Normal file
10
Tests/SimpleConf/TestConfigs/Parsing-OnlyKey.conf
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# A simple config file that contains only the key on a single line
|
||||||
|
|
||||||
|
normal_key=normal_value
|
||||||
|
another_key=another_value
|
||||||
|
generic=generic
|
||||||
|
another_one=another_value
|
||||||
|
a_very_basic_key
|
||||||
|
another_one=...
|
||||||
|
|
||||||
|
# It doesn't really matter what's here, because the parser won't reach it
|
53
Tests/SimpleConf/WhitespaceBehaviour.cpp
Normal file
53
Tests/SimpleConf/WhitespaceBehaviour.cpp
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <Garbage/SimpleConf.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Check behavior when whitespace is involved")
|
||||||
|
{
|
||||||
|
SECTION("Simple multi-line config")
|
||||||
|
{
|
||||||
|
using Required = Garbage::SimpleConf::Required;
|
||||||
|
|
||||||
|
std::string rawConfig =
|
||||||
|
" key with whitespace = and value too \n"
|
||||||
|
"another=one\n"
|
||||||
|
" and yet = another";
|
||||||
|
|
||||||
|
Garbage::SimpleConf config(std::move(rawConfig));
|
||||||
|
|
||||||
|
REQUIRE(config.Get<std::string, Required>("key with whitespace") == "and value too");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("another") == "one");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("and yet") == "another");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Deal with CRLF correctly")
|
||||||
|
{
|
||||||
|
using Required = Garbage::SimpleConf::Required;
|
||||||
|
|
||||||
|
std::string rawConfig =
|
||||||
|
" simple key=simple value \r\n"
|
||||||
|
" another = one\r\n"
|
||||||
|
" and one = more\r\n";
|
||||||
|
|
||||||
|
Garbage::SimpleConf config(std::move(rawConfig));
|
||||||
|
|
||||||
|
REQUIRE(config.Get<std::string, Required>("simple key") == "simple value");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("another") == "one");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("and one") == "more");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Weirder whitespace shenanigans")
|
||||||
|
{
|
||||||
|
using Required = Garbage::SimpleConf::Required;
|
||||||
|
|
||||||
|
std::string rawConfig =
|
||||||
|
"let's+start_simple=with some\tbasics\n"
|
||||||
|
"\t\tnow we get \tsome=\t \tweirder\tstuff\r\n"
|
||||||
|
"\t\tinsa+nity\twith\t\t=\t white \t space \t\t";
|
||||||
|
|
||||||
|
Garbage::SimpleConf config(std::move(rawConfig));
|
||||||
|
|
||||||
|
REQUIRE(config.Get<std::string, Required>("let's+start_simple") == "with some\tbasics");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("now we get \tsome") == "weirder\tstuff");
|
||||||
|
REQUIRE(config.Get<std::string, Required>("insa+nity\twith") == "white \t space");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue