Files
Breeze/source/tests/CoreTests/main.cpp
T
2026-10-24 09:53:48 -03:00

501 lines
17 KiB
C++

// A small harness and the tests of breeze_core's logic: text, versions, CMake editing, projects and
// their settings, modules, argument handling and process running. A test is a function that checks;
// the program exits with the number of failed checks (0 when all hold).
#include <filesystem>
#include <functional>
#include <iostream>
#include <source_location>
#include <string>
#include <vector>
#include "breeze/Arguments.hpp"
#include "breeze/CMakeText.hpp"
#include "breeze/Error.hpp"
#include "breeze/Fs.hpp"
#include "breeze/Modules.hpp"
#include "breeze/Process.hpp"
#include "breeze/Project.hpp"
#include "breeze/Text.hpp"
#include "breeze/Version.hpp"
namespace
{
int g_failures = 0;
void check(bool condition, std::source_location where = std::source_location::current())
{
if (!condition)
{
++g_failures;
std::cerr << where.file_name() << ":" << where.line() << ": check failed\n";
}
}
template <typename A, typename E>
void checkEq(const A& actual, const E& expected,
std::source_location where = std::source_location::current())
{
if (!(actual == expected))
{
++g_failures;
std::cerr << where.file_name() << ":" << where.line() << ": was '" << actual
<< "', expected '" << expected << "'\n";
}
}
template <typename F> bool throwsError(F&& f)
{
try
{
f();
}
catch (const breeze::Error&)
{
return true;
}
return false;
}
namespace sfs = std::filesystem;
struct TempDir
{
TempDir() = default;
TempDir(const TempDir&) = delete;
TempDir& operator=(const TempDir&) = delete;
TempDir(TempDir&&) = delete;
TempDir& operator=(TempDir&&) = delete;
sfs::path path = breeze::fs::makeTempDir("breeze-unit");
~TempDir()
{
std::error_code ec;
sfs::remove_all(path, ec);
}
};
void write(const sfs::path& file, const std::string& text)
{
breeze::fs::writeText(file, text);
}
// A project with the breeze/ layout and these settings and modules.
sfs::path makeProject(const sfs::path& root)
{
write(root / "CMakeLists.txt", "cmake_minimum_required(VERSION 3.25)\n");
write(root / "breeze" / "cmake" / "Breeze.cmake", "# sdk\n");
write(root / "breeze" / "cmake" / "BuildHelpers.cmake",
"macro(add_objects_module NAME)\nendmacro()\n");
write(root / "breeze" / "VERSION", "1.2.3\n");
write(root / "project" / "Configuration.cmake",
"# settings\n"
"set(PROJECT_TITLE \"Demo\")\n"
"set(PROJECT_VERSION_NUMBER \"0.1.0\")\n"
"set(PROJECT_CODE_NAME \"demo\" CACHE STRING\n"
" \"A machine-friendly name.\")\n");
return root;
}
// -----------------------------------------------------------------------------------------
// text
void textHelpers()
{
using namespace breeze::text;
checkEq(snakeCase("CoreFarewell"), "core_farewell");
checkEq(snakeCase("HelloApp"), "hello_app");
checkEq(snakeCase("HTTPServer"), "http_server");
checkEq(snakeCase("my-app"), "my_app");
checkEq(snakeCase("already_snake"), "already_snake");
checkEq(snakeCase("Util"), "util");
checkEq(normalized("Clang-CL"), "clangcl");
checkEq(normalized("MinGW64"), "mingw64");
check(isName("Core"));
check(isName("my_mod2"));
check(!isName("9lives"));
check(!isName("bad name"));
check(!isName("with-dash"));
check(isName("with-dash", true));
check(isTargetId("fmt::fmt"));
check(isTargetId("core_lib"));
check(!isTargetId("bad id"));
checkEq(trim(" x \n"), "x");
checkEq(join({"a", "b", "c"}, ", "), "a, b, c");
checkEq(replaceAll("a-b-c", "-", "+"), "a+b+c");
checkEq(splitLines("one\r\ntwo\nthree\n").size(), std::size_t(3));
check(startsWith("breeze", "bre"));
check(endsWith("breeze", "eze"));
}
// --------------------------------------------------------------------------------------
// versions
void versions()
{
using namespace breeze;
checkEq(parseVersion("1.2.3")->toString(), "1.2.3");
checkEq(parseVersion("v1.2.3")->toString(), "1.2.3");
checkEq(parseVersion("1.2")->toString(), "1.2.0");
checkEq(parseVersion("2")->toString(), "2.0.0");
checkEq(parseVersion("1.0.0-rc1")->toString(), "1.0.0-rc1");
checkEq(parseVersion("1.0.0+build5")->toString(), "1.0.0");
check(!parseVersion("a.b.c"));
check(!parseVersion("1.2.3.4"));
check(!parseVersion(""));
check(!parseVersion("notaversion"));
check(*parseVersion("1.0.0-rc1") < *parseVersion("1.0.0"));
check(*parseVersion("1.9.0") < *parseVersion("1.10.0"));
check(*parseVersion("1.2.3") == *parseVersion("v1.2.3"));
std::vector<Version> all{*parseVersion("1.0.0"), *parseVersion("1.2.0"),
*parseVersion("2.0.0"), *parseVersion("1.1.5")};
checkEq(greatest(all)->toString(), "2.0.0");
checkEq(greatest(all, 1)->toString(), "1.2.0");
check(!greatest(all, 3));
}
// ----------------------------------------------------------------------------------- cmake
// text
void cmakeText()
{
using namespace breeze::cmake;
std::string text = "# add_exe_module(in_a_comment)\n"
"message(\"add_exe_module(in_a_string)\")\n"
"#[[ add_exe_module(in_a_bracket_comment) ]]\n"
"add_exe_module(real_one OUTPUT_NAME \"real\"\n"
" DEPENDS a # trailing comment\n"
" b::c)\n"
"set(NAME \"old value\" CACHE STRING\n"
" \"doc\")\n";
auto wanted = [](const std::string& n) { return n == "add_exe_module"; };
std::vector<Call> calls = findCalls(text, wanted);
checkEq(calls.size(), std::size_t(1));
if (calls.size() == 1)
{
checkEq(calls[0].args.size(), std::size_t(6));
checkEq(calls[0].args[0].text, "real_one");
checkEq(calls[0].args[2].text, "real");
check(calls[0].args[2].quoted);
checkEq(calls[0].args[5].text, "b::c");
}
std::optional<Call> set = findSet(text, "NAME");
check(set.has_value());
if (set)
{
checkEq(set->args[1].text, "old value");
std::string edited = text;
replaceToken(edited, set->args[1], quote("new \"value\""));
check(edited.find("set(NAME \"new \\\"value\\\"\" CACHE STRING\n \"doc\")") !=
std::string::npos);
check(edited.find("# add_exe_module(in_a_comment)") !=
std::string::npos); // everything else untouched
}
check(!findSet(text, "MISSING"));
}
// ------------------------------------------------------------------------------------ projects
void projectDetection()
{
using namespace breeze;
TempDir t;
makeProject(t.path);
std::optional<Sdk> sdk = detectSdk(t.path);
check(sdk.has_value());
if (sdk)
{
check(sdk->layout == SdkLayout::Breeze);
checkEq(sdk->version->toString(), "1.2.3");
check(sdk->supportsObjects);
checkEq(sdk->describe(), "1.2.3");
}
write(t.path / "source" / "Mod" / "x.cpp", "");
std::optional<Project> found = findProject(t.path / "source" / "Mod");
check(found.has_value());
if (found)
check(found->root == sfs::weakly_canonical(t.path));
TempDir empty;
check(!detectSdk(empty.path));
check(throwsError([&] { requireProject(empty.path); }));
// the layout before breeze/: scripts/, cmake/ and Configuration.cmake at the root
TempDir legacy;
write(legacy.path / "CMakeLists.txt", "project(Old VERSION 3.4.5)\n");
write(legacy.path / "Configuration.cmake", "set(PROJECT_CODE_NAME \"old\")\n");
write(legacy.path / "cmake" / "BuildHelpers.cmake", "# no objects\n");
std::optional<Sdk> old = detectSdk(legacy.path);
check(old.has_value());
if (old)
{
check(old->layout == SdkLayout::Legacy);
check(!old->supportsObjects);
check(!old->version.has_value());
}
}
void settings()
{
using namespace breeze;
TempDir t;
makeProject(t.path);
Project p = *findProject(t.path);
checkEq(readSetting(p, Setting::Name), "Demo");
checkEq(readSetting(p, Setting::Version), "0.1.0");
checkEq(readSetting(p, Setting::Id), "demo");
writeSetting(p, Setting::Name, "Renamed");
writeSetting(p, Setting::Version, "2.0.1");
writeSetting(p, Setting::Id, "renamed_id");
checkEq(readSetting(p, Setting::Name), "Renamed");
checkEq(readSetting(p, Setting::Version), "2.0.1");
checkEq(readSetting(p, Setting::Id), "renamed_id");
std::string file = fs::readText(p.sdk.settingsFile);
check(file.find("CACHE STRING\n \"A machine-friendly name.\")") !=
std::string::npos); // the rest is kept
check(throwsError([&] { writeSetting(p, Setting::Version, "1.x"); }));
check(throwsError([&] { writeSetting(p, Setting::Id, "Upper"); }));
check(throwsError([&] { writeSetting(p, Setting::Name, "bad name"); }));
TempDir legacy;
write(legacy.path / "CMakeLists.txt",
"cmake_minimum_required(VERSION 3.25)\nproject(Old VERSION 3.4.5 LANGUAGES CXX)\n");
write(legacy.path / "Configuration.cmake",
"set(PROJECT_CODE_NAME \"old\" CACHE STRING \"x\")\n");
write(legacy.path / "cmake" / "BuildHelpers.cmake", "");
Project lp = *findProject(legacy.path);
checkEq(readSetting(lp, Setting::Name), "Old");
checkEq(readSetting(lp, Setting::Version), "3.4.5");
checkEq(readSetting(lp, Setting::Id), "old");
writeSetting(lp, Setting::Version, "4.0");
writeSetting(lp, Setting::Name, "Newer");
check(fs::readText(legacy.path / "CMakeLists.txt")
.find("project(Newer VERSION 4.0 LANGUAGES CXX)") != std::string::npos);
}
void defaults()
{
using namespace breeze;
TempDir t;
makeProject(t.path);
Project p = *findProject(t.path);
Defaults d = Defaults::load(p);
check(!d.build("preset"));
d.setBuild("preset", "linux-x64-gcc");
d.setRun("hello_exe");
d.save(p);
Defaults again = Defaults::load(p);
checkEq(*again.build("preset"), "linux-x64-gcc");
checkEq(*again.run(), "hello_exe");
again.clearBuild("preset");
again.clearRun();
again.save(p);
check(!sfs::exists(p.defaultsFile())); // nothing left to keep: the file goes
}
// ------------------------------------------------------------------------------------ modules
void modules()
{
using namespace breeze;
TempDir t;
makeProject(t.path);
write(t.path / "source" / "Core" / "CMakeLists.txt",
"add_lib_module(core_lib TYPE SHARED OUTPUT_NAME \"core\" DEPENDS fmt::fmt "
"core_pieces)\n");
write(t.path / "source" / "CorePieces" / "CMakeLists.txt",
"add_objects_module(core_pieces)\n");
write(t.path / "source" / "App" / "CMakeLists.txt",
"add_exe_module(app_exe DEPENDS core_lib)\n");
write(t.path / "source" / "tests" / "CoreTest" / "CMakeLists.txt",
"add_test_module(core_test DEPENDS core_pieces)\n");
Project p = *findProject(t.path);
std::vector<Module> mods = scanModules(p);
checkEq(mods.size(), std::size_t(4));
const Module* core = findModule(mods, "core_lib");
check(core != nullptr);
if (core)
{
check(core->kind == ModuleKind::Lib);
check(core->libType == LibType::Shared);
checkEq(core->outputName, "core");
checkEq(core->deps.size(), std::size_t(2));
}
check(findModule(mods, "CorePieces") != nullptr); // by directory
check(findModule(mods, "corepieces") != nullptr); // by directory, another case
check(findModule(mods, "core_pieces")->kind == ModuleKind::Obj);
check(findModule(mods, "core_test")->kind == ModuleKind::Test);
// dependency queries
checkEq(dependenciesOf(mods, *findModule(mods, "app_exe"), false).size(), std::size_t(1));
std::vector<std::string> all = dependenciesOf(mods, *findModule(mods, "app_exe"), true);
checkEq(all.size(), std::size_t(3)); // core_lib, fmt::fmt, core_pieces
checkEq(dependentsOf(mods, *findModule(mods, "core_pieces"), false).size(), std::size_t(2));
checkEq(dependentsOf(mods, *findModule(mods, "core_pieces"), true).size(), std::size_t(3));
// adding
NewModule lib;
lib.kind = ModuleKind::Lib;
lib.name = "Strings";
lib.libType = LibType::Static;
lib.deps = {"core_pieces", "fmt::fmt"};
Module made = addModule(p, lib);
checkEq(made.id, "strings_lib");
check(sfs::exists(t.path / "source" / "Strings" / "include" / "strings" / "Strings.hpp"));
check(sfs::exists(t.path / "source" / "Strings" / "src" / "Strings.cpp"));
NewModule test;
test.kind = ModuleKind::Test;
test.name = "StringsTest";
checkEq(addModule(p, test).id, "strings_test"); // not strings_test_test
check(sfs::exists(t.path / "source" / "tests" / "StringsTest" / "main.cpp"));
NewModule noType;
noType.kind = ModuleKind::Lib;
noType.name = "NoType";
check(throwsError([&] { addModule(p, noType); }));
NewModule duplicate;
duplicate.kind = ModuleKind::Exec;
duplicate.name = "Other";
duplicate.id = "app_exe";
check(throwsError([&] { addModule(p, duplicate); }));
NewModule onExe;
onExe.kind = ModuleKind::Exec;
onExe.name = "OnExe";
onExe.deps = {"app_exe"};
check(throwsError([&] { addModule(p, onExe); }));
NewModule badName;
badName.kind = ModuleKind::Exec;
badName.name = "9bad";
check(throwsError([&] { addModule(p, badName); }));
// renaming: the id, the directory, and the DEPENDS of whoever names it
RenameResult r =
renameModule(p, "core_pieces", std::string("Pieces"), std::string("pieces_obj"));
checkEq(r.newId, "pieces_obj");
check(sfs::exists(t.path / "source" / "Pieces" / "CMakeLists.txt"));
check(!sfs::exists(t.path / "source" / "CorePieces"));
mods = scanModules(p);
check(findModule(mods, "pieces_obj") != nullptr);
check(findModule(mods, "core_pieces") == nullptr);
checkEq(findModule(mods, "core_lib")->deps[1], "pieces_obj");
checkEq(findModule(mods, "core_test")->deps[0], "pieces_obj");
checkEq(findModule(mods, "strings_lib")->deps[0], "pieces_obj");
check(throwsError([&] { renameModule(p, "app_exe", std::nullopt, std::nullopt); }));
check(throwsError([&]
{ renameModule(p, "app_exe", std::nullopt, std::string("core_lib")); }));
// deleting: refused while something depends on it
check(throwsError([&] { deleteModule(p, "pieces_obj"); }));
deleteModule(p, "app_exe");
check(!sfs::exists(t.path / "source" / "App"));
check(throwsError([&] { deleteModule(p, "app_exe"); }));
}
void objectsNeedSdkSupport()
{
using namespace breeze;
TempDir t;
makeProject(t.path);
write(t.path / "breeze" / "cmake" / "BuildHelpers.cmake",
"# an SDK from before objects modules\n");
Project p = *findProject(t.path);
NewModule obj;
obj.kind = ModuleKind::Obj;
obj.name = "Pieces";
check(throwsError([&] { addModule(p, obj); }));
}
// ------------------------------------------------------------------------------------
// arguments
void arguments()
{
using breeze::cli::normalizeArguments;
using V = std::vector<std::string>;
check(normalizeArguments({"build", "--cargs", "-DA=1", "-DB=2", "-j", "4"}) ==
V({"build", "--cargs=-DA=1", "--cargs=-DB=2", "-j", "4"}));
check(normalizeArguments({"run", "hello_exe", "-x", "--verbose", "-q"}) ==
V({"run", "hello_exe", "--args=--verbose", "--args=-q"}));
check(normalizeArguments({"build", "--cargs=-DA=1"}) ==
V({"build", "--cargs=-DA=1"})); // already in the form
check(normalizeArguments({"build", "--cargs"}) ==
V({"build", "--cargs"})); // nothing: CLIForge reports it
check(normalizeArguments({"ls", "--cargs", "-D"}) ==
V({"ls", "--cargs", "-D"})); // not a build command
check(normalizeArguments({}).empty());
}
// ------------------------------------------------------------------------------------
// processes
void processes()
{
#ifndef _WIN32
using namespace breeze;
proc::Options o;
o.capture = true;
proc::Result r = proc::run({"sh", "-c", "echo out; echo err >&2; exit 3"}, o);
checkEq(r.exitCode, 3);
checkEq(r.output, "out\n");
o.mergeStderr = true;
r = proc::run({"sh", "-c", "echo out; echo err >&2"}, o);
check(r.output.find("err") != std::string::npos);
proc::Options env;
env.capture = true;
env.env = {{"BREEZE_TEST_VARIABLE", "present"}};
checkEq(proc::run({"sh", "-c", "printf %s \"$BREEZE_TEST_VARIABLE\""}, env).output,
"present");
TempDir t;
proc::Options in;
in.capture = true;
in.cwd = t.path;
checkEq(
sfs::weakly_canonical(
proc::run({"pwd"}, in).output.substr(0, proc::run({"pwd"}, in).output.size() - 1)),
sfs::weakly_canonical(t.path));
proc::Options quiet;
quiet.silent = true;
checkEq(proc::run({"sh", "-c", "echo hidden; exit 5"}, quiet).exitCode, 5);
checkEq(proc::run({"/no/such/program"}).exitCode, 127);
check(proc::findInPath("sh").has_value());
check(!proc::findInPath("no-such-program-anywhere").has_value());
#endif
}
struct Case
{
const char* name;
std::function<void()> run;
};
}
int main()
{
const std::vector<Case> cases = {
{"text helpers", textHelpers}, {"versions", versions},
{"cmake text", cmakeText}, {"project detection", projectDetection},
{"settings", settings}, {"defaults", defaults},
{"modules", modules}, {"objects modules need SDK support", objectsNeedSdkSupport},
{"arguments", arguments}, {"processes", processes},
};
for (const Case& c : cases)
{
int before = g_failures;
try
{
c.run();
}
catch (const std::exception& e)
{
++g_failures;
std::cerr << c.name << ": unexpected exception: " << e.what() << "\n";
}
std::cout << (g_failures == before ? "ok " : "FAIL ") << c.name << "\n";
}
std::cout << (g_failures == 0 ? "all tests passed"
: std::to_string(g_failures) + " check(s) failed")
<< "\n";
return g_failures == 0 ? 0 : 1;
}