// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors // // test/test_ipc.cpp — end-to-end test for the M1 IPC layer (§2.1, ADR-0004). // Launches the sandboxed document process, opens a real PDF via the RPC // client, and verifies the page count + ping round-trip. This is the M1 // foundation test: it proves the process split, the Cap'n Proto RPC over a // socketpair, the seccomp sandbox, the SCM_RIGHTS fd handoff, and the QPDF // read-from-handed-fd path all work together. // // Plain freestanding main (no GTest yet — matches the M0 test convention in // test/CMakeLists.txt). Returns non-zero on failure. #include "DocumentProcessClient.h" #include "ProcessLauncher.h" #include #include #include #include #include #include namespace { // A minimal valid one-page PDF with a correct xref (QPDF --check clean). // Written to a temp file by the test so the UI process can open() it and hand // the fd to the sandboxed document process. const char* kMinimalPdf = "%PDF-1.4\n1 0 obj<>endobj\n" "2 0 obj<>endobj\n" "3 0 obj<>endobj\n" "xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n" "0000000052 00000 n \n0000000101 00000 n \n" "trailer<>\nstartxref\n164\n%%EOF\n"; std::string write_temp_pdf() { char tmpl[] = "/tmp/fpe_ipc_test_XXXXXX"; int fd = ::mkstemp(tmpl); if (fd < 0) { std::perror("mkstemp"); std::exit(2); } // Append the .pdf extension to the mkstemp-generated name. std::string path = std::string(tmpl) + ".pdf"; ::close(fd); ::unlink(tmpl); // mkstemp created the placeholder; we want the .pdf name { std::ofstream f; f.open(path, std::ios::binary | std::ios::trunc); f << kMinimalPdf; } return path; } int fail(const char* msg) { std::fprintf(stderr, "FAIL: %s\n", msg); return 1; } } // namespace int main() { std::string path = write_temp_pdf(); auto proc = freepdfeditor::ipc::launch_document_process(path.c_str()); if (!proc.client) { ::unlink(path.c_str()); return fail("launch_document_process returned null client"); } // ping first — cheapest liveness check. try { if (!proc.client->ping("hello-ipc")) { ::unlink(path.c_str()); return fail("ping echo mismatch"); } } catch (const std::exception& e) { ::unlink(path.c_str()); std::fprintf(stderr, "FAIL: ping threw: %s\n", e.what()); return 1; } // open — should report 1 page (the minimal PDF has one page). freepdfeditor::ipc::OpenReply rep; try { rep = proc.client->open(path); } catch (const std::exception& e) { ::unlink(path.c_str()); std::fprintf(stderr, "FAIL: open threw: %s\n", e.what()); return 1; } if (!rep.ok) { ::unlink(path.c_str()); std::fprintf(stderr, "FAIL: open returned ok=false: %s\n", rep.error.c_str()); return 1; } if (rep.pageCount != 1) { ::unlink(path.c_str()); std::fprintf(stderr, "FAIL: expected 1 page, got %u\n", rep.pageCount); return 1; } if (rep.pdfVersion.empty()) { ::unlink(path.c_str()); return fail("empty pdfVersion"); } // getPageCount — should agree with open. try { std::uint32_t n = proc.client->getPageCount(); if (n != 1) { ::unlink(path.c_str()); std::fprintf(stderr, "FAIL: getPageCount expected 1, got %u\n", n); return 1; } } catch (const std::exception& e) { ::unlink(path.c_str()); std::fprintf(stderr, "FAIL: getPageCount threw: %s\n", e.what()); return 1; } // Destroying the client disconnects and reaps the child. proc.client.reset(); std::printf("PASS: ipc end-to-end — ping ok, open ok (pageCount=%u, " "pdfVersion=%s), getPageCount ok\n", rep.pageCount, rep.pdfVersion.c_str()); ::unlink(path.c_str()); return 0; }