Implement a simple config file parser

This commit is contained in:
TennesseeTrash 2025-06-09 01:41:33 +02:00
parent 405b68b824
commit 167b8bee8d
20 changed files with 601 additions and 0 deletions

View 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
)

View 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");
}

View 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);
}
}

View 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));
}
}

View 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);
}
}

View 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]"));
}
}

View 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

View 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

View 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

View file

View file

@ -0,0 +1,2 @@
# This probably doesn't need to exist, but eh
existing key = existing value

View 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

View 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

View 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

View file

@ -0,0 +1,4 @@
# A simple config
that appears = correct
but it = has = an invalid line # Should blow up here
another=pair

View 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

View 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");
}
}