// 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 #include #include #include #include #include #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 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 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 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 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 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 = 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 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 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 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 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; 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 run; }; } int main() { const std::vector 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; }