freepdfeditor/test/test_ipc.cpp

129 lines
4.1 KiB
C++

// 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 <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <unistd.h>
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<</Type/Catalog/Pages 2 0 R>>endobj\n"
"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n"
"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj\n"
"xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n"
"0000000052 00000 n \n0000000101 00000 n \n"
"trailer<</Size 4/Root 1 0 R>>\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;
}