Skip to content

Commit

Permalink
Replace createTemporaryNodeServer with TestPdfsServer
Browse files Browse the repository at this point in the history
Some tests rely on the presence of a server that serves PDF files.
When tests are run from a web browser, the test files and PDF files are
served by the same server (WebServer), but in Node.js that server is not
around.

Currently, the tests that depend on it start a minimal Node.js server
that re-implements part of the functionality from WebServer.

To avoid code duplication when tests depend on more complex behaviors,
this patch replaces createTemporaryNodeServer with the existing
WebServer, wrapped in a new test utility that has the same interface in
Node.js and non-Node.js environments (=TestPdfsServer).

This patch has been tested by running the refactored tests in the
following three configurations:

1. From the browser:
   - http://localhost:8888/test/unit/unit_test.html?spec=api
   - http://localhost:8888/test/unit/unit_test.html?spec=fetch_stream

2. Run specific tests directly with jasmine without legacy bundling:
   `JASMINE_CONFIG_PATH=test/unit/clitests.json ./node_modules/.bin/jasmine --filter='^api|^fetch_stream'`

3. `gulp unittestcli`
  • Loading branch information
Rob--W committed Nov 24, 2024
1 parent d45a61b commit 5ce54c7
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 95 deletions.
24 changes: 6 additions & 18 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import {
import {
buildGetDocumentParams,
CMAP_URL,
createTemporaryNodeServer,
DefaultFileReaderFactory,
TEST_PDFS_PATH,
TestPdfsServer,
} from "./test_utils.js";
import {
DefaultCanvasFactory,
Expand Down Expand Up @@ -67,27 +67,17 @@ describe("api", function () {
buildGetDocumentParams(tracemonkeyFileName);

let CanvasFactory;
let tempServer = null;

beforeAll(function () {
beforeAll(async function () {
CanvasFactory = new DefaultCanvasFactory({});

if (isNodeJS) {
tempServer = createTemporaryNodeServer();
}
await TestPdfsServer.ensureStarted();
});

afterAll(function () {
afterAll(async function () {
CanvasFactory = null;

if (isNodeJS) {
// Close the server from accepting new connections after all test
// finishes.
const { server } = tempServer;
server.close();

tempServer = null;
}
await TestPdfsServer.ensureStopped();
});

function waitSome(callback) {
Expand Down Expand Up @@ -148,9 +138,7 @@ describe("api", function () {
});

it("creates pdf doc from URL-object", async function () {
const urlObj = isNodeJS
? new URL(`http://127.0.0.1:${tempServer.port}/${basicApiFileName}`)
: new URL(TEST_PDFS_PATH + basicApiFileName, window.location);
const urlObj = TestPdfsServer.resolveURL(basicApiFileName);

const loadingTask = getDocument(urlObj);
expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true);
Expand Down
27 changes: 7 additions & 20 deletions test/unit/fetch_stream_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,22 @@
* limitations under the License.
*/

import { AbortException, isNodeJS } from "../../src/shared/util.js";
import { createTemporaryNodeServer } from "./test_utils.js";
import { AbortException } from "../../src/shared/util.js";
import { PDFFetchStream } from "../../src/display/fetch_stream.js";
import { TestPdfsServer } from "./test_utils.js";

describe("fetch_stream", function () {
let tempServer = null;

function getPdfUrl() {
return isNodeJS
? `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf`
: new URL("../pdfs/tracemonkey.pdf", window.location).href;
return TestPdfsServer.resolveURL("tracemonkey.pdf").href;
}
const pdfLength = 1016315;

beforeAll(function () {
if (isNodeJS) {
tempServer = createTemporaryNodeServer();
}
beforeAll(async function () {
await TestPdfsServer.ensureStarted();
});

afterAll(function () {
if (isNodeJS) {
// Close the server from accepting new connections after all test
// finishes.
const { server } = tempServer;
server.close();

tempServer = null;
}
afterAll(async function () {
await TestPdfsServer.ensureStopped();
});

it("read with streaming", async function () {
Expand Down
132 changes: 75 additions & 57 deletions test/unit/test_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,73 +122,91 @@ function createIdFactory(pageIndex) {
return page._localIdFactory;
}

function createTemporaryNodeServer() {
assert(isNodeJS, "Should only be used in Node.js environments.");

const fs = process.getBuiltinModule("fs"),
http = process.getBuiltinModule("http");
function isAcceptablePath(requestUrl) {
try {
// Reject unnormalized paths, to protect against path traversal attacks.
const url = new URL(requestUrl, "https://localhost/");
return url.pathname === requestUrl;
} catch {
return false;
// Some tests rely on special behavior from webserver.mjs. When loaded in the
// browser, the page is already served from WebServer. When running from
// Node.js, that is not the case. This helper starts the WebServer if needed,
// and offers a mechanism to resolve the URL in a uniform way.
class TestPdfsServer {
static #webServer;

static #startCount = 0;

static async ensureStarted() {
if (this.#startCount++) {
// Already started before. E.g. from another beforeAll call.
return;
}
if (!isNodeJS) {
// In web browsers, tests are presumably served by webserver.mjs.
return;
}

// WebServer from webserver.mjs is imported dynamically instead of
// statically because we do not need it when running from the browser.
let WebServer;
if (import.meta.url.endsWith("/lib-legacy/test/unit/test_utils.js")) {
// When "gulp unittestcli" is used to run tests, the tests are run from
// pdf.js/build/lib-legacy/test/ instead of directly from pdf.js/test/.
// eslint-disable-next-line import/no-unresolved
({ WebServer } = await import("../../../../test/webserver.mjs"));
} else {
({ WebServer } = await import("../webserver.mjs"));
}
this.#webServer = new WebServer({
host: "127.0.0.1",
root: TEST_PDFS_PATH,
});
await new Promise(resolve => {
this.#webServer.start(resolve);
});
}

static async ensureStopped() {
assert(this.#startCount > 0, "ensureStarted() should be called first");
if (--this.#startCount) {
// Keep server alive as long as there is an ensureStarted() that was not
// followed by an ensureStopped() call.
// This could happen if ensureStarted() was called again before
// ensureStopped() was called from afterAll().
return;
}
if (!isNodeJS) {
// Web browsers cannot stop the server.
return;
}

await new Promise(resolve => {
this.#webServer.stop(resolve);
this.#webServer = null;
});
}

/**
* @param {string} path - path to file within test/unit/pdf/ (TEST_PDFS_PATH).
* @returns {URL}
*/
static resolveURL(path) {
assert(this.#startCount > 0, "ensureStarted() should be called first");

if (isNodeJS) {
// Note: TestPdfsServer.ensureStarted() should be called first.
return new URL(path, `http://127.0.0.1:${this.#webServer.port}/`);
}
// When "gulp server" is used, our URL looks like
// http://localhost:8888/test/unit/unit_test.html
// The PDFs are served from:
// http://localhost:8888/test/pdfs/
return new URL(TEST_PDFS_PATH + path, window.location);
}
// Create http server to serve pdf data for tests.
const server = http
.createServer((request, response) => {
if (!isAcceptablePath(request.url)) {
response.writeHead(400);
response.end("Invalid path");
return;
}
const filePath = process.cwd() + "/test/pdfs" + request.url;
fs.promises.lstat(filePath).then(
stat => {
if (!request.headers.range) {
const contentLength = stat.size;
const stream = fs.createReadStream(filePath);
response.writeHead(200, {
"Content-Type": "application/pdf",
"Content-Length": contentLength,
"Accept-Ranges": "bytes",
});
stream.pipe(response);
} else {
const [start, end] = request.headers.range
.split("=")[1]
.split("-")
.map(x => Number(x));
const stream = fs.createReadStream(filePath, { start, end });
response.writeHead(206, {
"Content-Type": "application/pdf",
});
stream.pipe(response);
}
},
error => {
response.writeHead(404);
response.end(`File ${request.url} not found!`);
}
);
})
.listen(0); /* Listen on a random free port */

return {
server,
port: server.address().port,
};
}

export {
buildGetDocumentParams,
CMAP_URL,
createIdFactory,
createTemporaryNodeServer,
DefaultFileReaderFactory,
STANDARD_FONT_DATA_URL,
TEST_PDFS_PATH,
TestPdfsServer,
XRefMock,
};

0 comments on commit 5ce54c7

Please sign in to comment.