Replace createTemporaryNodeServer with TestPdfsServer

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`
This commit is contained in:
Rob Wu 2024-11-24 23:05:59 +01:00
parent d45a61b579
commit 28b0220bc2
3 changed files with 98 additions and 94 deletions

View File

@ -31,9 +31,9 @@ import {
import { import {
buildGetDocumentParams, buildGetDocumentParams,
CMAP_URL, CMAP_URL,
createTemporaryNodeServer,
DefaultFileReaderFactory, DefaultFileReaderFactory,
TEST_PDFS_PATH, TEST_PDFS_PATH,
TestPdfsServer,
} from "./test_utils.js"; } from "./test_utils.js";
import { import {
DefaultCanvasFactory, DefaultCanvasFactory,
@ -67,27 +67,17 @@ describe("api", function () {
buildGetDocumentParams(tracemonkeyFileName); buildGetDocumentParams(tracemonkeyFileName);
let CanvasFactory; let CanvasFactory;
let tempServer = null;
beforeAll(function () { beforeAll(async function () {
CanvasFactory = new DefaultCanvasFactory({}); CanvasFactory = new DefaultCanvasFactory({});
if (isNodeJS) { await TestPdfsServer.ensureStarted();
tempServer = createTemporaryNodeServer();
}
}); });
afterAll(function () { afterAll(async function () {
CanvasFactory = null; CanvasFactory = null;
if (isNodeJS) { await TestPdfsServer.ensureStopped();
// Close the server from accepting new connections after all test
// finishes.
const { server } = tempServer;
server.close();
tempServer = null;
}
}); });
function waitSome(callback) { function waitSome(callback) {
@ -148,9 +138,7 @@ describe("api", function () {
}); });
it("creates pdf doc from URL-object", async function () { it("creates pdf doc from URL-object", async function () {
const urlObj = isNodeJS const urlObj = TestPdfsServer.resolveURL(basicApiFileName);
? new URL(`http://127.0.0.1:${tempServer.port}/${basicApiFileName}`)
: new URL(TEST_PDFS_PATH + basicApiFileName, window.location);
const loadingTask = getDocument(urlObj); const loadingTask = getDocument(urlObj);
expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true);

View File

@ -13,35 +13,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { AbortException, isNodeJS } from "../../src/shared/util.js"; import { AbortException } from "../../src/shared/util.js";
import { createTemporaryNodeServer } from "./test_utils.js";
import { PDFFetchStream } from "../../src/display/fetch_stream.js"; import { PDFFetchStream } from "../../src/display/fetch_stream.js";
import { TestPdfsServer } from "./test_utils.js";
describe("fetch_stream", function () { describe("fetch_stream", function () {
let tempServer = null;
function getPdfUrl() { function getPdfUrl() {
return isNodeJS return TestPdfsServer.resolveURL("tracemonkey.pdf").href;
? `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf`
: new URL("../pdfs/tracemonkey.pdf", window.location).href;
} }
const pdfLength = 1016315; const pdfLength = 1016315;
beforeAll(function () { beforeAll(async function () {
if (isNodeJS) { await TestPdfsServer.ensureStarted();
tempServer = createTemporaryNodeServer();
}
}); });
afterAll(function () { afterAll(async function () {
if (isNodeJS) { await TestPdfsServer.ensureStopped();
// Close the server from accepting new connections after all test
// finishes.
const { server } = tempServer;
server.close();
tempServer = null;
}
}); });
it("read with streaming", async function () { it("read with streaming", async function () {

View File

@ -122,73 +122,102 @@ function createIdFactory(pageIndex) {
return page._localIdFactory; return page._localIdFactory;
} }
function createTemporaryNodeServer() { // Some tests rely on special behavior from webserver.mjs. When loaded in the
assert(isNodeJS, "Should only be used in Node.js environments."); // 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;
const fs = process.getBuiltinModule("fs"), static #startCount = 0;
http = process.getBuiltinModule("http");
function isAcceptablePath(requestUrl) { static #startPromise;
try {
// Reject unnormalized paths, to protect against path traversal attacks. static async ensureStarted() {
const url = new URL(requestUrl, "https://localhost/"); if (this.#startCount++) {
return url.pathname === requestUrl; // Already started before. E.g. from another beforeAll call.
} catch { return this.#startPromise;
return false; }
if (!isNodeJS) {
// In web browsers, tests are presumably served by webserver.mjs.
return undefined;
} }
}
// 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 { this.#startPromise = this.#startServer().finally(() => {
server, this.#startPromise = null;
port: server.address().port, });
}; return this.#startPromise;
}
static async #startServer() {
// 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");
assert(!this.#startPromise, "ensureStarted() should have resolved");
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");
assert(!this.#startPromise, "ensureStarted() should have resolved");
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);
}
} }
export { export {
buildGetDocumentParams, buildGetDocumentParams,
CMAP_URL, CMAP_URL,
createIdFactory, createIdFactory,
createTemporaryNodeServer,
DefaultFileReaderFactory, DefaultFileReaderFactory,
STANDARD_FONT_DATA_URL, STANDARD_FONT_DATA_URL,
TEST_PDFS_PATH, TEST_PDFS_PATH,
TestPdfsServer,
XRefMock, XRefMock,
}; };