// This is a tester for Hyprland. It will launch the built binary in ./build/Hyprland // in headless mode and test various things. // for now it's quite basic and limited, but will be expanded in the future. // NOTE: This tester has to be ran from its directory!! // Some TODO: // - Add a plugin built alongside so that we can do more detailed tests (e.g. simulating keystrokes) // - test coverage // - maybe figure out a way to do some visual tests too? // Required runtime deps for checks: // - kitty // - xeyes #include "shared.hpp" #include "hyprctlCompat.hpp" #include "tests/main/tests.hpp" #include "tests/plugin/plugin.hpp" #include #include #include #include #include #include #include #include #include "Log.hpp" using namespace Hyprutils::OS; using namespace Hyprutils::Memory; #define SP CSharedPointer static int ret = 0; static SP hyprlandProc; static const std::string cwd = std::filesystem::current_path().string(); // static bool launchHyprland(std::string configPath, std::string binaryPath) { if (binaryPath == "") { std::error_code ec; if (!std::filesystem::exists(cwd + "/../build/Hyprland", ec) || ec) { NLog::log("{}No Hyprland binary", Colors::RED); return false; } binaryPath = cwd + "/../build/Hyprland"; } if (configPath == "") { std::error_code ec; if (!std::filesystem::exists(cwd + "/test.conf", ec) || ec) { NLog::log("{}No test config", Colors::RED); return false; } configPath = cwd + "/test.conf"; } NLog::log("{}Launching Hyprland", Colors::YELLOW); hyprlandProc = makeShared(binaryPath, std::vector{"--config", configPath}); hyprlandProc->addEnv("HYPRLAND_HEADLESS_ONLY", "1"); NLog::log("{}Launched async process", Colors::YELLOW); return hyprlandProc->runAsync(); } static bool hyprlandAlive() { NLog::log("{}hyprlandAlive", Colors::YELLOW); kill(hyprlandProc->pid(), 0); return errno != ESRCH; } static void help() { NLog::log("usage: hyprtester [arg [...]].\n"); NLog::log(R"(Arguments: --help -h - Show this message again --config FILE -c FILE - Specify config file to use --binary FILE -b FILE - Specify Hyprland binary to use --plugin FILE -p FILE - Specify the location of the test plugin)"); } int main(int argc, char** argv, char** envp) { std::string configPath = ""; std::string binaryPath = ""; std::string pluginPath = std::filesystem::current_path().string(); std::vector args{argv + 1, argv + argc}; for (auto it = args.begin(); it != args.end(); it++) { if (*it == "--config" || *it == "-c") { if (std::next(it) == args.end()) { help(); return 1; } configPath = *std::next(it); try { configPath = std::filesystem::canonical(configPath); if (!std::filesystem::is_regular_file(configPath)) { throw std::exception(); } } catch (...) { std::println(stderr, "[ ERROR ] Config file '{}' doesn't exist!", configPath); help(); return 1; } it++; continue; } else if (*it == "--binary" || *it == "-b") { if (std::next(it) == args.end()) { help(); return 1; } binaryPath = *std::next(it); try { binaryPath = std::filesystem::canonical(binaryPath); if (!std::filesystem::is_regular_file(binaryPath)) { throw std::exception(); } } catch (...) { std::println(stderr, "[ ERROR ] Binary '{}' doesn't exist!", binaryPath); help(); return 1; } it++; continue; } else if (*it == "--plugin" || *it == "-p") { if (std::next(it) == args.end()) { help(); return 1; } pluginPath = *std::next(it); try { pluginPath = std::filesystem::canonical(pluginPath); if (!std::filesystem::is_regular_file(pluginPath)) { throw std::exception(); } } catch (...) { std::println(stderr, "[ ERROR ] plugin '{}' doesn't exist!", pluginPath); help(); return 1; } it++; continue; } else if (*it == "--help" || *it == "-h") { help(); return 0; } else { std::println(stderr, "[ ERROR ] Unknown option '{}' !", *it); help(); return 1; } } NLog::log("{}launching hl", Colors::YELLOW); if (!launchHyprland(configPath, binaryPath)) { NLog::log("{}well it failed", Colors::RED); return 1; } // hyprland has launched, let's check if it's alive after 10s std::this_thread::sleep_for(std::chrono::milliseconds(10000)); NLog::log("{}slept for 10s", Colors::YELLOW); if (!hyprlandAlive()) { NLog::log("{}Hyprland failed to launch", Colors::RED); return 1; } // wonderful, we are in. Let's get the instance signature. NLog::log("{}trying to get INSTANCES", Colors::YELLOW); const auto INSTANCES = instances(); if (INSTANCES.empty()) { NLog::log("{}Hyprland failed to launch (2)", Colors::RED); return 1; } HIS = INSTANCES.back().id; WLDISPLAY = INSTANCES.back().wlSocket; NLog::log("{}trying to get create headless output", Colors::YELLOW); getFromSocket("/output create headless"); NLog::log("{}trying to load plugin", Colors::YELLOW); if (const auto R = getFromSocket(std::format("/plugin load {}", pluginPath)); R != "ok") { NLog::log("{}Failed to load the test plugin: {}", Colors::RED, R); getFromSocket("/dispatch exit 1"); return 1; } NLog::log("{}Loaded plugin", Colors::YELLOW); for (const auto& fn : testFns) { EXPECT(fn(), true); } NLog::log("{}running plugin test", Colors::YELLOW); EXPECT(testPlugin(), true); // kill hyprland NLog::log("{}dispatching exit", Colors::YELLOW); getFromSocket("/dispatch exit"); NLog::log("\n{}Summary:\n\tPASSED: {}{}{}/{}\n\tFAILED: {}{}{}/{}\n{}", Colors::RESET, Colors::GREEN, TESTS_PASSED, Colors::RESET, TESTS_PASSED + TESTS_FAILED, Colors::RED, TESTS_FAILED, Colors::RESET, TESTS_PASSED + TESTS_FAILED, (TESTS_FAILED > 0 ? std::string{Colors::RED} + "\nSome tests failed.\n" : "")); kill(hyprlandProc->pid(), SIGKILL); hyprlandProc.reset(); return ret || TESTS_FAILED; }