♻️ refactor(cli): validate startup arguments
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
cmake_minimum_required(VERSION 4.0)
|
||||
|
||||
project(test_args_parser LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
|
||||
|
||||
find_package(spdlog CONFIG REQUIRED)
|
||||
find_package(fmt CONFIG REQUIRED)
|
||||
|
||||
add_executable(test_args_parser main.cc test_args_parser.cppm)
|
||||
if(TARGET std_module)
|
||||
add_dependencies(test_args_parser std_module)
|
||||
endif()
|
||||
|
||||
target_sources(
|
||||
test_args_parser
|
||||
PRIVATE
|
||||
FILE_SET cxx_modules TYPE CXX_MODULES
|
||||
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
FILES test_args_parser.cppm
|
||||
../../src/bridge/spdlog.cppm
|
||||
../../src/utils/args_parser.cppm)
|
||||
|
||||
target_compile_definitions(test_args_parser PRIVATE SPDLOG_HEADER_ONLY
|
||||
FMT_HEADER_ONLY)
|
||||
target_link_libraries(test_args_parser PRIVATE spdlog::spdlog_header_only
|
||||
fmt::fmt-header-only)
|
||||
target_compile_options(
|
||||
test_args_parser
|
||||
PRIVATE -Wall -Wextra -Wpedantic
|
||||
-Wno-import-implementation-partition-unit-in-interface-unit
|
||||
$<$<CONFIG:Debug>:-g -O0>
|
||||
$<$<CONFIG:Release>:-O3>)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_test(NAME test_args_parser COMMAND $<TARGET_FILE:test_args_parser>)
|
||||
endif()
|
||||
@@ -0,0 +1,6 @@
|
||||
import lsp.test.args_parser;
|
||||
|
||||
int main()
|
||||
{
|
||||
return Run();
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
module;
|
||||
|
||||
export module lsp.test.args_parser;
|
||||
|
||||
import spdlog;
|
||||
import std;
|
||||
|
||||
import lsp.utils.args_parser;
|
||||
|
||||
namespace
|
||||
{
|
||||
void Expect(bool condition, std::string_view message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(std::string(message));
|
||||
}
|
||||
|
||||
auto Parse(std::initializer_list<std::string_view> arguments)
|
||||
{
|
||||
std::vector<std::string> storage{ "tsl-server" };
|
||||
storage.reserve(arguments.size() + 1);
|
||||
for (auto argument : arguments)
|
||||
storage.emplace_back(argument);
|
||||
|
||||
std::vector<char*> argv;
|
||||
argv.reserve(storage.size());
|
||||
for (auto& argument : storage)
|
||||
argv.push_back(argument.data());
|
||||
|
||||
return lsp::utils::ParseArgs(static_cast<int>(argv.size()), argv.data());
|
||||
}
|
||||
|
||||
void ExpectError(std::initializer_list<std::string_view> arguments,
|
||||
std::string_view fragment)
|
||||
{
|
||||
auto result = Parse(arguments);
|
||||
Expect(!result.has_value(), "arguments should be rejected");
|
||||
Expect(result.error().find(fragment) != std::string::npos,
|
||||
"error should identify the invalid argument");
|
||||
}
|
||||
}
|
||||
|
||||
export int Run()
|
||||
{
|
||||
int failures = 0;
|
||||
auto RunCase = [&failures](std::string_view name, auto test) {
|
||||
try
|
||||
{
|
||||
test();
|
||||
std::cout << "[PASS] " << name << '\n';
|
||||
}
|
||||
catch (const std::exception& error)
|
||||
{
|
||||
++failures;
|
||||
std::cout << "[FAIL] " << name << ": " << error.what() << '\n';
|
||||
}
|
||||
};
|
||||
|
||||
RunCase("defaults", [] {
|
||||
auto result = Parse({});
|
||||
Expect(result.has_value(), "default arguments should parse");
|
||||
Expect(result->action == lsp::utils::ParseAction::kRun,
|
||||
"default action should run");
|
||||
Expect(result->config.thread_count == 4, "default thread count should be 4");
|
||||
Expect(result->config.log_level == spdlog::level::info,
|
||||
"default log level should be info");
|
||||
});
|
||||
|
||||
RunCase("valid values and last occurrence", [] {
|
||||
auto result = Parse({ "--threads=8", "--threads=16", "--log=debug", "--log-file=server.log", "--interpreter=/opt/tsl" });
|
||||
Expect(result.has_value(), "valid arguments should parse");
|
||||
Expect(result->config.thread_count == 16, "last thread count should win");
|
||||
Expect(result->config.log_level == spdlog::level::debug,
|
||||
"debug log level should parse");
|
||||
Expect(result->config.log_file == "server.log", "log file should parse");
|
||||
Expect(result->config.interpreter_path == "/opt/tsl",
|
||||
"interpreter path should parse");
|
||||
});
|
||||
|
||||
RunCase("all log levels", [] {
|
||||
const std::array levels{
|
||||
std::pair{ "trace", spdlog::level::trace },
|
||||
std::pair{ "debug", spdlog::level::debug },
|
||||
std::pair{ "info", spdlog::level::info },
|
||||
std::pair{ "warn", spdlog::level::warn },
|
||||
std::pair{ "error", spdlog::level::err },
|
||||
std::pair{ "off", spdlog::level::off },
|
||||
};
|
||||
for (const auto& [name, level] : levels)
|
||||
{
|
||||
const std::string argument = std::string("--log=") + name;
|
||||
auto result = Parse({ argument });
|
||||
Expect(result.has_value(), "known log level should parse");
|
||||
Expect(result->config.log_level == level, "log level should match");
|
||||
}
|
||||
});
|
||||
|
||||
RunCase("help takes precedence", [] {
|
||||
auto result = Parse({ "--unknown", "--help", "--threads=bad" });
|
||||
Expect(result.has_value(), "help should bypass other validation");
|
||||
Expect(result->action == lsp::utils::ParseAction::kShowHelp,
|
||||
"help action should be returned");
|
||||
});
|
||||
|
||||
RunCase("thread validation", [] {
|
||||
Expect(Parse({ "--threads=1" }).has_value(), "one thread should parse");
|
||||
Expect(Parse({ "--threads=256" }).has_value(), "256 threads should parse");
|
||||
for (auto argument : { "--threads=", "--threads=0", "--threads=-1", "--threads=257", "--threads=12junk", "--threads=18446744073709551616" })
|
||||
ExpectError({ argument }, "--threads");
|
||||
});
|
||||
|
||||
RunCase("other invalid values", [] {
|
||||
for (auto argument : { "--log=verbose", "--log-file=", "--interpreter=", "--log-stdout", "--log-stderr", "--use-stdio", "--unknown" })
|
||||
ExpectError({ argument }, "--");
|
||||
});
|
||||
|
||||
RunCase("help formatting", [] {
|
||||
std::ostringstream output;
|
||||
lsp::utils::PrintHelp(output, "tsl-server");
|
||||
const auto text = output.str();
|
||||
Expect(text.contains("Usage: tsl-server [options]\n\nOptions:\n"),
|
||||
"help should contain physical line breaks");
|
||||
Expect(!text.contains("\\n"), "help should not contain escaped newline text");
|
||||
Expect(!text.contains("--log-stdout"), "removed stdout flag should be absent");
|
||||
Expect(!text.contains("--log-stderr"), "removed stderr flag should be absent");
|
||||
Expect(!text.contains("--use-stdio"), "removed stdio alias should be absent");
|
||||
});
|
||||
|
||||
std::cout << "Failures: " << failures << '\n';
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user