// testserver_fixture.hpp — spawn tools/testserver for a test, tear it down after. // // Linux-only (fork/exec/pipe/kill). The path to testserver.py is injected by CMake as // VDM_TESTSERVER_PY; if it's empty or missing the fixture reports unavailable() and the // test should skip. #ifndef VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP #define VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP #include #include #include #include #include #include #include #include #include #include #include #ifndef VDM_TESTSERVER_PY #define VDM_TESTSERVER_PY "" #endif namespace vdm::testing { class TestServer { public: TestServer() : TestServer(1.0) {} // loris_seconds overrides the dribble duration slow-loris mode uses (default matches the // no-arg ctor's long-standing 1s). A test that needs curl's stall detector // (CURLOPT_LOW_SPEED_TIME, hardcoded to 30s in download_task.cpp) to actually fire needs a // dribble that outlasts that threshold, not the short one every other test relies on to // keep runtime down. explicit TestServer(double loris_seconds) { const char *script = VDM_TESTSERVER_PY; if (!script || !*script || ::access(script, R_OK) != 0) return; int pipefd[2]; if (::pipe(pipefd) != 0) return; pid_ = ::fork(); if (pid_ < 0) { ::close(pipefd[0]); ::close(pipefd[1]); return; } if (pid_ == 0) { ::dup2(pipefd[1], STDOUT_FILENO); ::close(pipefd[0]); ::close(pipefd[1]); int devnull = ::open("/dev/null", O_WRONLY); if (devnull >= 0) ::dup2(devnull, STDERR_FILENO); std::string loris_str = std::to_string(loris_seconds); ::execlp("python3", "python3", script, "--port", "0", "--seed", "9", "--loris-seconds", loris_str.c_str(), "--throttle-bps", "131072", static_cast(nullptr)); ::_exit(127); } ::close(pipefd[1]); // Read the port line the server prints to stdout. std::string line; char c = 0; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (std::chrono::steady_clock::now() < deadline) { ssize_t r = ::read(pipefd[0], &c, 1); if (r == 1) { if (c == '\n') break; line += c; } else if (r == 0) { break; } else if (errno != EINTR) { break; } } ::close(pipefd[0]); if (!line.empty()) port_ = std::atoi(line.c_str()); // Give the listener a moment to accept. std::this_thread::sleep_for(std::chrono::milliseconds(150)); } ~TestServer() { if (pid_ > 0) { ::kill(pid_, SIGTERM); int status = 0; for (int i = 0; i < 50; ++i) { if (::waitpid(pid_, &status, WNOHANG) == pid_) return; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } ::kill(pid_, SIGKILL); ::waitpid(pid_, &status, 0); } } TestServer(const TestServer &) = delete; TestServer &operator=(const TestServer &) = delete; [[nodiscard]] bool available() const { return port_ > 0; } [[nodiscard]] int port() const { return port_; } [[nodiscard]] std::string url(const std::string &path) const { return "http://127.0.0.1:" + std::to_string(port_) + path; } private: pid_t pid_ = -1; int port_ = 0; }; } // namespace vdm::testing #endif // VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP