Merge pull request #17263 from timvandermeij/font-tests
Introduce a GitHub Actions workflow for running the font tests
This commit is contained in:
commit
44cde3ccca
64
.github/workflows/font_tests.yml
vendored
Normal file
64
.github/workflows/font_tests.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Font tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'gulpfile.mjs'
|
||||||
|
- 'src/**'
|
||||||
|
- 'test/test.mjs'
|
||||||
|
- 'test/font/**'
|
||||||
|
- '.github/workflows/font_tests.yml'
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'gulpfile.mjs'
|
||||||
|
- 'src/**'
|
||||||
|
- 'test/test.mjs'
|
||||||
|
- 'test/font/**'
|
||||||
|
- '.github/workflows/font_tests.yml'
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
node-version: [lts/*]
|
||||||
|
os: [windows-latest, ubuntu-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install Gulp
|
||||||
|
run: npm install -g gulp-cli
|
||||||
|
|
||||||
|
- name: Install other dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Use Python 3.12
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install Fonttools
|
||||||
|
run: pip install fonttools
|
||||||
|
|
||||||
|
- name: Run font tests
|
||||||
|
run: gulp fonttest --headless
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "test/ttx/fonttools-code"]
|
|
||||||
path = test/ttx/fonttools-code
|
|
||||||
url = https://github.com/behdad/fonttools.git
|
|
@ -684,6 +684,9 @@ function createTestSource(testsName, { bot = false, xfaOnly = false } = {}) {
|
|||||||
if (process.argv.includes("--noChrome") || forceNoChrome) {
|
if (process.argv.includes("--noChrome") || forceNoChrome) {
|
||||||
args.push("--noChrome");
|
args.push("--noChrome");
|
||||||
}
|
}
|
||||||
|
if (process.argv.includes("--headless")) {
|
||||||
|
args.push("--headless");
|
||||||
|
}
|
||||||
|
|
||||||
const testProcess = startNode(args, { cwd: TEST_DIR, stdio: "inherit" });
|
const testProcess = startNode(args, { cwd: TEST_DIR, stdio: "inherit" });
|
||||||
testProcess.on("close", function (code) {
|
testProcess.on("close", function (code) {
|
||||||
@ -712,6 +715,9 @@ function makeRef(done, bot) {
|
|||||||
if (process.argv.includes("--noChrome") || forceNoChrome) {
|
if (process.argv.includes("--noChrome") || forceNoChrome) {
|
||||||
args.push("--noChrome");
|
args.push("--noChrome");
|
||||||
}
|
}
|
||||||
|
if (process.argv.includes("--headless")) {
|
||||||
|
args.push("--headless");
|
||||||
|
}
|
||||||
|
|
||||||
const testProcess = startNode(args, { cwd: TEST_DIR, stdio: "inherit" });
|
const testProcess = startNode(args, { cwd: TEST_DIR, stdio: "inherit" });
|
||||||
testProcess.on("close", function (code) {
|
testProcess.on("close", function (code) {
|
||||||
@ -1743,7 +1749,6 @@ gulp.task(
|
|||||||
return streamqueue(
|
return streamqueue(
|
||||||
{ objectMode: true },
|
{ objectMode: true },
|
||||||
createTestSource("unit", { bot: true }),
|
createTestSource("unit", { bot: true }),
|
||||||
createTestSource("font", { bot: true }),
|
|
||||||
createTestSource("browser", { bot: true }),
|
createTestSource("browser", { bot: true }),
|
||||||
createTestSource("integration")
|
createTestSource("integration")
|
||||||
);
|
);
|
||||||
@ -1768,7 +1773,6 @@ gulp.task(
|
|||||||
return streamqueue(
|
return streamqueue(
|
||||||
{ objectMode: true },
|
{ objectMode: true },
|
||||||
createTestSource("unit", { bot: true }),
|
createTestSource("unit", { bot: true }),
|
||||||
createTestSource("font", { bot: true }),
|
|
||||||
createTestSource("browser", { bot: true, xfaOnly: true }),
|
createTestSource("browser", { bot: true, xfaOnly: true }),
|
||||||
createTestSource("integration")
|
createTestSource("integration")
|
||||||
);
|
);
|
||||||
|
36
test/font/README.md
Normal file
36
test/font/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Font tests
|
||||||
|
|
||||||
|
The font tests check if PDF.js can read font data correctly. For validation
|
||||||
|
the `ttx` tool (from the Python `fonttools` library) is used that can convert
|
||||||
|
font data to an XML format that we can easily use for assertions in the tests.
|
||||||
|
In the font tests we let PDF.js read font data and pass the PDF.js-interpreted
|
||||||
|
font data through `ttx` to check its correctness. The font tests are successful
|
||||||
|
if PDF.js can successfully read the font data and `ttx` can successfully read
|
||||||
|
the PDF.js-interpreted font data back, proving that PDF.js does not apply any
|
||||||
|
transformations that break the font data.
|
||||||
|
|
||||||
|
## Running the font tests
|
||||||
|
|
||||||
|
The font tests are run on GitHub Actions using the workflow defined in
|
||||||
|
`.github/workflows/font_tests.yml`, but it is also possible to run the font
|
||||||
|
tests locally. The current stable versions of the following dependencies are
|
||||||
|
required to be installed on the system:
|
||||||
|
|
||||||
|
- Python 3
|
||||||
|
- `fonttools` (see https://pypi.org/project/fonttools and https://github.com/fonttools/fonttools)
|
||||||
|
|
||||||
|
The recommended way of installing `fonttools` is using `pip` in a virtual
|
||||||
|
environment because it avoids having to do a system-wide installation and
|
||||||
|
therefore improves isolation, but any other way of installing `fonttools`
|
||||||
|
that makes `ttx` available in the `PATH` environment variable also works.
|
||||||
|
|
||||||
|
Using the virtual environment approach the font tests can be run locally by
|
||||||
|
creating and sourcing a virtual environment with `fonttools` installed in
|
||||||
|
it before running the font tests:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install fonttools
|
||||||
|
gulp fonttest
|
||||||
|
```
|
@ -14,65 +14,43 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
let ttxTaskId = Date.now();
|
||||||
|
|
||||||
const ttxResourcesHome = path.join(__dirname, "..", "ttx");
|
function runTtx(fontPath, registerOnCancel, callback) {
|
||||||
|
const ttx = spawn("ttx", [fontPath], { stdio: "ignore" });
|
||||||
let nextTTXTaskId = Date.now();
|
let ttxRunError;
|
||||||
|
registerOnCancel(function (reason) {
|
||||||
function runTtx(ttxResourcesHomePath, fontPath, registerOnCancel, callback) {
|
ttxRunError = reason;
|
||||||
fs.realpath(ttxResourcesHomePath, function (error, realTtxResourcesHomePath) {
|
callback(reason);
|
||||||
const fontToolsHome = path.join(realTtxResourcesHomePath, "fonttools-code");
|
ttx.kill();
|
||||||
fs.realpath(fontPath, function (errorFontPath, realFontPath) {
|
});
|
||||||
const ttxPath = path.join("Lib", "fontTools", "ttx.py");
|
ttx.on("error", function (errorTtx) {
|
||||||
if (!fs.existsSync(path.join(fontToolsHome, ttxPath))) {
|
ttxRunError = errorTtx;
|
||||||
callback("TTX was not found, please checkout PDF.js submodules");
|
callback(
|
||||||
return;
|
"Unable to execute `ttx`; make sure the `fonttools` dependency is installed"
|
||||||
}
|
);
|
||||||
const ttxEnv = {
|
});
|
||||||
PYTHONPATH: path.join(fontToolsHome, "Lib"),
|
ttx.on("close", function (code) {
|
||||||
PYTHONDONTWRITEBYTECODE: true,
|
if (ttxRunError) {
|
||||||
};
|
return;
|
||||||
const ttxStdioMode = "ignore";
|
}
|
||||||
const python = process.platform !== "win32" ? "python2" : "python";
|
callback();
|
||||||
const ttx = spawn(python, [ttxPath, realFontPath], {
|
|
||||||
cwd: fontToolsHome,
|
|
||||||
stdio: ttxStdioMode,
|
|
||||||
env: ttxEnv,
|
|
||||||
});
|
|
||||||
let ttxRunError;
|
|
||||||
registerOnCancel(function (reason) {
|
|
||||||
ttxRunError = reason;
|
|
||||||
callback(reason);
|
|
||||||
ttx.kill();
|
|
||||||
});
|
|
||||||
ttx.on("error", function (errorTtx) {
|
|
||||||
ttxRunError = errorTtx;
|
|
||||||
callback("Unable to execute ttx");
|
|
||||||
});
|
|
||||||
ttx.on("close", function (code) {
|
|
||||||
if (ttxRunError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function translateFont(content, registerOnCancel, callback) {
|
function translateFont(content, registerOnCancel, callback) {
|
||||||
const buffer = Buffer.from(content, "base64");
|
const buffer = Buffer.from(content, "base64");
|
||||||
const taskId = (nextTTXTaskId++).toString();
|
const taskId = (ttxTaskId++).toString();
|
||||||
const fontPath = path.join(ttxResourcesHome, taskId + ".otf");
|
const fontPath = path.join(os.tmpdir(), `pdfjs-font-test-${taskId}.otf`);
|
||||||
const resultPath = path.join(ttxResourcesHome, taskId + ".ttx");
|
const resultPath = path.join(os.tmpdir(), `pdfjs-font-test-${taskId}.ttx`);
|
||||||
|
|
||||||
fs.writeFileSync(fontPath, buffer);
|
fs.writeFileSync(fontPath, buffer);
|
||||||
runTtx(ttxResourcesHome, fontPath, registerOnCancel, function (err) {
|
runTtx(fontPath, registerOnCancel, function (err) {
|
||||||
fs.unlinkSync(fontPath);
|
fs.unlinkSync(fontPath);
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
116
test/test.mjs
116
test/test.mjs
@ -99,6 +99,12 @@ function parseOptions() {
|
|||||||
describe: "Uses default answers (intended for CLOUD TESTS only!).",
|
describe: "Uses default answers (intended for CLOUD TESTS only!).",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
})
|
||||||
|
.option("headless", {
|
||||||
|
default: false,
|
||||||
|
describe:
|
||||||
|
"Run the tests in headless mode, i.e. without visible browser windows.",
|
||||||
|
type: "boolean",
|
||||||
|
})
|
||||||
.option("port", {
|
.option("port", {
|
||||||
default: 0,
|
default: 0,
|
||||||
describe: "The port the HTTP server should listen on.",
|
describe: "The port the HTTP server should listen on.",
|
||||||
@ -250,8 +256,11 @@ function updateRefImages() {
|
|||||||
function examineRefImages() {
|
function examineRefImages() {
|
||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
const startUrl = `http://${host}:${server.port}/test/resources/reftest-analyzer.html#web=/test/eq.log`;
|
startBrowser({
|
||||||
startBrowser("firefox", startUrl).then(function (browser) {
|
browserName: "firefox",
|
||||||
|
headless: false,
|
||||||
|
startUrl: `http://${host}:${server.port}/test/resources/reftest-analyzer.html#web=/test/eq.log`,
|
||||||
|
}).then(function (browser) {
|
||||||
browser.on("disconnected", function () {
|
browser.on("disconnected", function () {
|
||||||
stopServer();
|
stopServer();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@ -339,26 +348,28 @@ function startRefTest(masterMode, showRefImages) {
|
|||||||
server.hooks.POST.push(refTestPostHandler);
|
server.hooks.POST.push(refTestPostHandler);
|
||||||
onAllSessionsClosed = finalize;
|
onAllSessionsClosed = finalize;
|
||||||
|
|
||||||
const startUrl = `http://${host}:${server.port}/test/test_slave.html`;
|
await startBrowsers({
|
||||||
await startBrowsers(function (session) {
|
baseUrl: `http://${host}:${server.port}/test/test_slave.html`,
|
||||||
session.masterMode = masterMode;
|
initializeSession: session => {
|
||||||
session.taskResults = {};
|
session.masterMode = masterMode;
|
||||||
session.tasks = {};
|
session.taskResults = {};
|
||||||
session.remaining = manifest.length;
|
session.tasks = {};
|
||||||
manifest.forEach(function (item) {
|
session.remaining = manifest.length;
|
||||||
var rounds = item.rounds || 1;
|
manifest.forEach(function (item) {
|
||||||
var roundsResults = [];
|
var rounds = item.rounds || 1;
|
||||||
roundsResults.length = rounds;
|
var roundsResults = [];
|
||||||
session.taskResults[item.id] = roundsResults;
|
roundsResults.length = rounds;
|
||||||
session.tasks[item.id] = item;
|
session.taskResults[item.id] = roundsResults;
|
||||||
});
|
session.tasks[item.id] = item;
|
||||||
session.numRuns = 0;
|
});
|
||||||
session.numErrors = 0;
|
session.numRuns = 0;
|
||||||
session.numFBFFailures = 0;
|
session.numErrors = 0;
|
||||||
session.numEqNoSnapshot = 0;
|
session.numFBFFailures = 0;
|
||||||
session.numEqFailures = 0;
|
session.numEqNoSnapshot = 0;
|
||||||
monitorBrowserTimeout(session, handleSessionTimeout);
|
session.numEqFailures = 0;
|
||||||
}, makeTestUrl(startUrl));
|
monitorBrowserTimeout(session, handleSessionTimeout);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function checkRefsTmp() {
|
function checkRefsTmp() {
|
||||||
if (masterMode && fs.existsSync(refsTmpDir)) {
|
if (masterMode && fs.existsSync(refsTmpDir)) {
|
||||||
@ -793,29 +804,18 @@ function onAllSessionsClosedAfterTests(name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTestUrl(startUrl) {
|
|
||||||
return function (browserName) {
|
|
||||||
const queryParameters =
|
|
||||||
`?browser=${encodeURIComponent(browserName)}` +
|
|
||||||
`&manifestFile=${encodeURIComponent("/test/" + options.manifestFile)}` +
|
|
||||||
`&testFilter=${JSON.stringify(options.testfilter)}` +
|
|
||||||
`&xfaOnly=${options.xfaOnly}` +
|
|
||||||
`&delay=${options.statsDelay}` +
|
|
||||||
`&masterMode=${options.masterMode}`;
|
|
||||||
return startUrl + queryParameters;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startUnitTest(testUrl, name) {
|
async function startUnitTest(testUrl, name) {
|
||||||
onAllSessionsClosed = onAllSessionsClosedAfterTests(name);
|
onAllSessionsClosed = onAllSessionsClosedAfterTests(name);
|
||||||
startServer();
|
startServer();
|
||||||
server.hooks.POST.push(unitTestPostHandler);
|
server.hooks.POST.push(unitTestPostHandler);
|
||||||
|
|
||||||
const startUrl = `http://${host}:${server.port}${testUrl}`;
|
await startBrowsers({
|
||||||
await startBrowsers(function (session) {
|
baseUrl: `http://${host}:${server.port}${testUrl}`,
|
||||||
session.numRuns = 0;
|
initializeSession: session => {
|
||||||
session.numErrors = 0;
|
session.numRuns = 0;
|
||||||
}, makeTestUrl(startUrl));
|
session.numErrors = 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startIntegrationTest() {
|
async function startIntegrationTest() {
|
||||||
@ -823,9 +823,12 @@ async function startIntegrationTest() {
|
|||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
const { runTests } = await import("./integration-boot.mjs");
|
const { runTests } = await import("./integration-boot.mjs");
|
||||||
await startBrowsers(function (session) {
|
await startBrowsers({
|
||||||
session.numRuns = 0;
|
baseUrl: null,
|
||||||
session.numErrors = 0;
|
initializeSession: session => {
|
||||||
|
session.numRuns = 0;
|
||||||
|
session.numErrors = 0;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
global.integrationBaseUrl = `http://${host}:${server.port}/build/generic/web/viewer.html`;
|
global.integrationBaseUrl = `http://${host}:${server.port}/build/generic/web/viewer.html`;
|
||||||
global.integrationSessions = sessions;
|
global.integrationSessions = sessions;
|
||||||
@ -901,10 +904,12 @@ function unitTestPostHandler(req, res) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startBrowser(browserName, startUrl = "") {
|
async function startBrowser({ browserName, headless, startUrl }) {
|
||||||
const options = {
|
const options = {
|
||||||
product: browserName,
|
product: browserName,
|
||||||
headless: false,
|
// Note that using `headless: true` gives a deprecation warning; see
|
||||||
|
// https://github.com/puppeteer/puppeteer#default-runtime-settings.
|
||||||
|
headless: headless === true ? "new" : false,
|
||||||
defaultViewport: null,
|
defaultViewport: null,
|
||||||
ignoreDefaultArgs: ["--disable-extensions"],
|
ignoreDefaultArgs: ["--disable-extensions"],
|
||||||
// The timeout for individual protocol (CDP) calls should always be lower
|
// The timeout for individual protocol (CDP) calls should always be lower
|
||||||
@ -971,7 +976,7 @@ async function startBrowser(browserName, startUrl = "") {
|
|||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startBrowsers(initSessionCallback, makeStartUrl = null) {
|
async function startBrowsers({ baseUrl, initializeSession }) {
|
||||||
// Remove old browser revisions from Puppeteer's cache. Updating Puppeteer can
|
// Remove old browser revisions from Puppeteer's cache. Updating Puppeteer can
|
||||||
// cause new browser revisions to be downloaded, so trimming the cache will
|
// cause new browser revisions to be downloaded, so trimming the cache will
|
||||||
// prevent the disk from filling up over time.
|
// prevent the disk from filling up over time.
|
||||||
@ -995,12 +1000,25 @@ async function startBrowsers(initSessionCallback, makeStartUrl = null) {
|
|||||||
closed: false,
|
closed: false,
|
||||||
};
|
};
|
||||||
sessions.push(session);
|
sessions.push(session);
|
||||||
const startUrl = makeStartUrl ? makeStartUrl(browserName) : "";
|
|
||||||
|
|
||||||
await startBrowser(browserName, startUrl)
|
// Construct the start URL from the base URL by appending query parameters
|
||||||
|
// for the runner if necessary.
|
||||||
|
let startUrl = "";
|
||||||
|
if (baseUrl) {
|
||||||
|
const queryParameters =
|
||||||
|
`?browser=${encodeURIComponent(browserName)}` +
|
||||||
|
`&manifestFile=${encodeURIComponent("/test/" + options.manifestFile)}` +
|
||||||
|
`&testFilter=${JSON.stringify(options.testfilter)}` +
|
||||||
|
`&xfaOnly=${options.xfaOnly}` +
|
||||||
|
`&delay=${options.statsDelay}` +
|
||||||
|
`&masterMode=${options.masterMode}`;
|
||||||
|
startUrl = baseUrl + queryParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
await startBrowser({ browserName, headless: options.headless, startUrl })
|
||||||
.then(function (browser) {
|
.then(function (browser) {
|
||||||
session.browser = browser;
|
session.browser = browser;
|
||||||
initSessionCallback?.(session);
|
initializeSession(session);
|
||||||
})
|
})
|
||||||
.catch(function (ex) {
|
.catch(function (ex) {
|
||||||
console.log(`Error while starting ${browserName}: ${ex.message}`);
|
console.log(`Error while starting ${browserName}: ${ex.message}`);
|
||||||
|
@ -1 +0,0 @@
|
|||||||
If `git clone --recursive` was not used, please run `git submodule init; git submodule update` to pull fonttools code.
|
|
@ -1 +0,0 @@
|
|||||||
Subproject commit d8170131a3458ffbc19089cf33249777bde390e7
|
|
Loading…
x
Reference in New Issue
Block a user