diff options
| author | Vaxry <43317083+vaxerski@users.noreply.github.com> | 2025-06-26 19:43:39 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-26 19:43:39 +0200 |
| commit | 3d6476c9021519995239ec93bbb11f0dce6c60a0 (patch) | |
| tree | 381edc199f4348e4bb42863b55d47a01e4f550ab /hyprtester/src/main.cpp | |
| parent | 9a67e0421b49596060719566996ee4ca81ae8843 (diff) | |
Core: Add a test suite (#9297)
Adds a test suite for testing hyprland's features with a runtime tester
---------
Co-authored-by: Mihai Fufezan <mihai@fufexan.net>
Diffstat (limited to 'hyprtester/src/main.cpp')
| -rw-r--r-- | hyprtester/src/main.cpp | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/hyprtester/src/main.cpp b/hyprtester/src/main.cpp new file mode 100644 index 00000000..176eb702 --- /dev/null +++ b/hyprtester/src/main.cpp @@ -0,0 +1,251 @@ + +// 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 <filesystem> +#include <hyprutils/os/Process.hpp> +#include <hyprutils/memory/WeakPtr.hpp> +#include <csignal> +#include <cerrno> +#include <chrono> +#include <thread> +#include <print> + +#include "Log.hpp" + +using namespace Hyprutils::OS; +using namespace Hyprutils::Memory; + +#define SP CSharedPointer + +static int ret = 0; +static SP<CProcess> 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<CProcess>(binaryPath, std::vector<std::string>{"--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<std::string> 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); + + // now we can start issuing stuff. + NLog::log("{}testing windows", Colors::YELLOW); + EXPECT(testWindows(), true); + + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + + NLog::log("{}testing groups", Colors::YELLOW); + EXPECT(testGroups(), true); + + NLog::log("{}testing workspaces", Colors::YELLOW); + EXPECT(testWorkspaces(), true); + + NLog::log("{}testing misc variables", Colors::YELLOW); + EXPECT(testMisc(), 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; +} |
