summaryrefslogtreecommitdiff
path: root/hyprtester/src/main.cpp
blob: 0da9a20e17a2ecb7cff2854d4fce6fdbb6ad2103 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284

// 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"
#undef TEST_CASES_STORAGE // Prevent redefinition warning
#include "tests/clients/tests.hpp"
#undef TEST_CASES_STORAGE // Prevent redefinition warning
#include "tests/plugin/plugin.hpp"
#include "tests/shared.hpp"

#include <algorithm>
#include <filesystem>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/Casts.hpp>

#include <csignal>
#include <cerrno>
#include <chrono>
#include <thread>
#include <print>
#include <string_view>
#include <span>

#include "Log.hpp"

using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
using Path = std::filesystem::path;

#define SP CSharedPointer

static SP<CProcess> hyprlandProc;

static bool         launchHyprland(Path configPath, Path binaryPath) {
    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);
    return kill(hyprlandProc->pid(), 0) == 0 || errno != ESRCH;
}

[[noreturn]] static void helpAndDie(int exit_code) {
    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 (default: './test.lua')
    --binary FILE       -b FILE  - Specify Hyprland binary to use (default: '../build/Hyprland')
    --plugin FILE       -p FILE  - Specify the location of the test plugin (default: './'))");

    std::exit(exit_code);
}

static Path validatePathOrDie(Path path) {
    try {
        if (!std::filesystem::is_regular_file(path)) {
            throw std::exception();
        }
    } catch (...) {
        std::println(stderr, "[ ERROR ] File '{}' is not accessible or not a regular file", path.string());
        helpAndDie(EXIT_FAILURE);
    }
    return path;
}

namespace {
    struct SSettings {
        Path configPath;
        Path binaryPath;
        Path pluginPath;
    };
}

static SSettings parseSettings(const std::span<const char*> args) {
    static const auto cwd = std::filesystem::current_path();
    SSettings         settings{};

    for (auto it = args.begin(); it < args.end(); it++) {
        std::string_view value = *it;

        if (value == "--config" || value == "-c") {
            if (std::next(it) == args.end()) {
                helpAndDie(EXIT_FAILURE);
            }

            settings.configPath = validatePathOrDie(*std::next(it));
            it++;
        } else if (value == "--binary" || value == "-b") {
            if (std::next(it) == args.end()) {
                helpAndDie(EXIT_FAILURE);
            }

            settings.binaryPath = validatePathOrDie(*std::next(it));
            it++;
        } else if (value == "--plugin" || value == "-p") {
            if (std::next(it) == args.end()) {
                helpAndDie(EXIT_FAILURE);
            }

            settings.pluginPath = validatePathOrDie(*std::next(it));
            it++;
        } else if (value == "--help" || value == "-h") {
            helpAndDie(EXIT_SUCCESS);
        } else {
            std::println(stderr, "[ ERROR ] Unknown option '{}' !", *it);
            helpAndDie(EXIT_SUCCESS);
        }
    }

    // Default options
    if (settings.configPath.empty())
        settings.configPath = validatePathOrDie(cwd / "test.lua");
    if (settings.binaryPath.empty())
        settings.binaryPath = validatePathOrDie(cwd / "../build/Hyprland");
    if (settings.pluginPath.empty())
        settings.pluginPath = cwd;

    return settings;
}

static bool preTestCleanup() {
    bool failed = false;

    if (!Tests::killAllWindows()) {
        NLog::log("{}Internal failure: failed to kill all windows", Colors::RED);
        failed = true;
    }
    if (!Tests::killAllLayers()) {
        NLog::log("{}Internal failure: failed to kill all layers", Colors::RED);
        failed = true;
    }
    if (getFromSocket("/reload") != "ok") {
        NLog::log("{}Internal failure: failed to reload", Colors::RED);
        failed = true;
    }
    if (!getFromSocket("/activeworkspace").contains("workspace ID 1 (1)")) {
        if (getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })") != "ok") {
            NLog::log("{}Internal failure: failed to switch to workspace 1", Colors::RED);
            failed = true;
        }
    }
    if (getFromSocket("/dispatch hl.dsp.cursor.move({ x = 960, y = 540 })") != "ok") {
        NLog::log("{}Internal failure: failed to reset cursor position", Colors::RED);
        failed = true;
    }

    return !failed;
}

static void runTests(std::map<const char*, CTestCase&>& testCases) {
    for (auto& [name, tc] : testCases) {
        // Clean up before every test
        NLog::log("{}Cleaning up", Colors::YELLOW);
        (void)preTestCleanup();

        NLog::log("{}Running test {}", Colors::BLUE, name);
        tc.test();
        if (tc.failed)
            NLog::log("{}Test failed: {}", Colors::RED, name);
        else
            NLog::log("{}Test passed: {}", Colors::GREEN, name);
    }
}

static long long countFailed(const std::map<const char*, CTestCase&>& testCases) {
    long long ans = 0;
    for (const auto& [_, tc] : testCases) {
        if (tc.failed)
            ans++;
    }
    return ans;
}

int main(int argc, char** argv, char** envp) {

    std::span<const char*> args{const_cast<const char**>(argv + 1), sc<std::size_t>(argc - 1)};
    const SSettings        settings = parseSettings(args);

    NLog::log("{}launching hl", Colors::YELLOW);
    if (!launchHyprland(settings.configPath, settings.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 {}", settings.pluginPath.string())); R != "ok") {
        NLog::log("{}Failed to load the test plugin: {}", Colors::RED, R);
        getFromSocket("/dispatch hl.dsp.exit()");
        return 1;
    }

    NLog::log("{}Loaded plugin", Colors::YELLOW);

    long long failedTests = 0, totalTests = 0;

    NLog::log("{}Running main tests", Colors::YELLOW);
    runTests(mainTestCases);
    failedTests += countFailed(mainTestCases);
    totalTests += mainTestCases.size();

    NLog::log("{}Running protocol client tests", Colors::YELLOW);
    runTests(clientTestCases);
    failedTests += countFailed(clientTestCases);
    totalTests += clientTestCases.size();

    // TODO: the two tests below should not be hardcoded, include them somewhere

    NLog::log("{}running plugin test", Colors::YELLOW);
    if (!testPlugin()) {
        NLog::log("{}Test failed: plugin test", Colors::RED);
        failedTests++;
    } else {
        NLog::log("{}Test passed: plugin test", Colors::GREEN);
    }
    totalTests++;

    NLog::log("{}running vkb test from plugin", Colors::YELLOW);
    if (!testVkb()) {
        NLog::log("{}Test failed: vkb test from plugin", Colors::RED);
        failedTests++;
    } else {
        NLog::log("{}Test passed: vkb test from plugin", Colors::GREEN);
    }
    totalTests++;

    // kill hyprland
    NLog::log("{}dispatching exit", Colors::YELLOW);
    getFromSocket("/dispatch hl.dsp.exit()");

    NLog::log("\nSummary:\n\tPASSED: {}{}{}/{}", Colors::GREEN, totalTests - failedTests, Colors::RESET, totalTests);
    NLog::log("\tFAILED: {}{}{}/{}", Colors::RED, failedTests, Colors::RESET, totalTests);
    if (failedTests > 0)
        NLog::log("{}Some tests failed.", Colors::RED);

    kill(hyprlandProc->pid(), SIGKILL);

    hyprlandProc.reset();

    return failedTests > 0;
}