// Integration test for velox-nmhost: spawns the real binary, feeds it a framed stdin // message, answers over a fake Unix socket standing in for veloxd, and checks what comes // back out on stdout — plus that it exits promptly once stdin closes. #include #include #include #include #include #include #include #include #include #include #include "check.hpp" namespace { std::string make_temp_dir() { char tmpl[] = "/tmp/velox-nmhost-test-XXXXXX"; const char* dir = ::mkdtemp(tmpl); return dir ? dir : "/tmp"; } // Firefox's own framing: 4-byte native-byte-order length, then that many bytes. std::string frame(const std::string& payload) { std::uint32_t len = static_cast(payload.size()); std::string out(reinterpret_cast(&len), 4); out += payload; return out; } // Reads exactly one framed message off `fd`, blocking. Empty on EOF/short read. std::string read_frame(int fd) { std::uint32_t len = 0; std::size_t got = 0; while (got < 4) { const ssize_t n = ::read(fd, reinterpret_cast(&len) + got, 4 - got); if (n <= 0) return {}; got += static_cast(n); } std::string payload(len, '\0'); got = 0; while (got < len) { const ssize_t n = ::read(fd, payload.data() + got, len - got); if (n <= 0) return {}; got += static_cast(n); } return payload; } bool write_all(int fd, const std::string& s) { std::size_t off = 0; while (off < s.size()) { const ssize_t n = ::write(fd, s.data() + off, s.size() - off); if (n <= 0) return false; off += static_cast(n); } return true; } } // namespace void run() { const std::string dir = make_temp_dir(); const std::string velox_dir = dir + "/velox"; CHECK(::mkdir(velox_dir.c_str(), 0700) == 0); const std::string sock_path = velox_dir + "/velox.sock"; // A bare listening socket standing in for veloxd. const int listen_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); CHECK(listen_fd >= 0); sockaddr_un addr{}; addr.sun_family = AF_UNIX; std::memcpy(addr.sun_path, sock_path.c_str(), sock_path.size()); CHECK(::bind(listen_fd, reinterpret_cast(&addr), sizeof(addr)) == 0); CHECK(::listen(listen_fd, 1) == 0); int child_stdin[2]; // [0] read (child), [1] write (parent) int child_stdout[2]; // [0] read (parent), [1] write (child) CHECK(::pipe(child_stdin) == 0); CHECK(::pipe(child_stdout) == 0); ::setenv("XDG_RUNTIME_DIR", dir.c_str(), 1); const pid_t pid = ::fork(); CHECK(pid >= 0); if (pid == 0) { ::dup2(child_stdin[0], 0); ::dup2(child_stdout[1], 1); ::close(child_stdin[0]); ::close(child_stdin[1]); ::close(child_stdout[0]); ::close(child_stdout[1]); ::close(listen_fd); ::execl(VELOX_NMHOST_BIN, "velox-nmhost", nullptr); ::_exit(127); } ::close(child_stdin[0]); ::close(child_stdout[1]); // Accept nmhost's connection. const int conn = ::accept(listen_fd, nullptr, nullptr); CHECK(conn >= 0); // stdin (framed) -> socket (NDJSON line). CHECK(write_all(child_stdin[1], frame(R"({"hello":1})"))); char line[256] = {}; ssize_t n = ::read(conn, line, sizeof(line) - 1); CHECK(n > 0); CHECK_EQ(std::string(line, static_cast(n)), std::string("{\"hello\":1}\n")); // socket (NDJSON line) -> stdout (framed). CHECK(write_all(conn, "{\"world\":2}\n")); const std::string got = read_frame(child_stdout[0]); CHECK_EQ(got, std::string(R"({"world":2})")); // A second round trip on the same connection, to prove buffering across calls works // (not just "the first message happens to line up with one read()"). CHECK(write_all(child_stdin[1], frame(R"({"again":3})"))); n = ::read(conn, line, sizeof(line) - 1); CHECK(n > 0); CHECK_EQ(std::string(line, static_cast(n)), std::string("{\"again\":3}\n")); // Firefox closes the pipe: nmhost must exit promptly rather than hang. ::close(child_stdin[1]); int status = 0; pid_t waited = -1; for (int i = 0; i < 50 && waited != pid; ++i) { waited = ::waitpid(pid, &status, WNOHANG); if (waited == pid) break; struct timespec ts{0, 20'000'000}; // 20ms ::nanosleep(&ts, nullptr); } CHECK_EQ(waited, pid); if (waited == pid) CHECK(WIFEXITED(status)); ::close(conn); ::close(child_stdout[0]); ::close(listen_fd); ::unlink(sock_path.c_str()); ::rmdir(velox_dir.c_str()); ::rmdir(dir.c_str()); } TEST_MAIN()