summaryrefslogtreecommitdiff
path: root/hyprtester/src/main.cpp
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2025-06-26 19:43:39 +0200
committerGitHub <noreply@github.com>2025-06-26 19:43:39 +0200
commit3d6476c9021519995239ec93bbb11f0dce6c60a0 (patch)
tree381edc199f4348e4bb42863b55d47a01e4f550ab /hyprtester/src/main.cpp
parent9a67e0421b49596060719566996ee4ca81ae8843 (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.cpp251
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;
+}