onnxruntime/js/web/test/e2e/run.js
Yulong Wang 0627a6cb93
[js/web] fix package export for bundlers (#23257)
### Description
<!-- Describe your changes. -->

This PR tries to fix #22615. (see detailed description in the issue)

A perfect solution would be too difficult to make, because there are a
huge number of combinations of usage scenarios, including combinations
of development framework, bundler, dev/prod mode, and so on.

This PR is using the following approach:
- Introduce a new type of end to end test: export test. This type of
tests are complete web apps that use popular web development frameworks,
and the tests are using puppeteer to run the apps and check if the apps
can run without error.
  - added one nextjs based web app and one vite based web app.
- In the test, perform the following test steps:
  - `npm install` for packages built locally
- `npm run dev` to start dev server and use puppeteer to launch the
browser to test
- `npm run build && npm run start` to test prod build and use puppeteer
to launch the browser to test
- Make changes to ort-web, including:
- special handling on Webpack's behavior of rewriting `import.meta.url`
to a `file://` string
  - revise build definitions
  - fix wasm URL for proxy, if used in a bundled build
2025-01-09 11:01:00 -08:00

244 lines
8.7 KiB
JavaScript

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
const path = require('path');
const fs = require('fs-extra');
const { spawn } = require('child_process');
const startServer = require('./simple-http-server');
const minimist = require('minimist');
const { NODEJS_TEST_CASES, BROWSER_TEST_CASES, BUNDLER_TEST_CASES } = require('./run-data');
// commandline arguments
const parsedArgs = minimist(process.argv.slice(2));
const BROWSER = parsedArgs.browser || 'Chrome_headless';
// Preserve the existing test folder
// When this flag is set, the test folder will not be cleared before running the tests.
// NPM install will not be run again.
// This is useful for debugging test failures as it makes it much faster to re-run.
const PRESERVE = parsedArgs.preserve || parsedArgs.p;
// copy whole folder to out-side of <ORT_ROOT>/js/ because we need to test in a folder that no `package.json` file
// exists in its parent folder.
// here we use <ORT_ROOT>/build/js/e2e/ for the test
const TEST_E2E_SRC_FOLDER = __dirname;
const JS_ROOT_FOLDER = path.resolve(__dirname, '../../..');
const TEST_E2E_RUN_FOLDER = path.resolve(JS_ROOT_FOLDER, '../build/js/e2e');
const NPM_CACHE_FOLDER = path.resolve(TEST_E2E_RUN_FOLDER, '../npm_cache');
const CHROME_USER_DATA_FOLDER = path.resolve(TEST_E2E_RUN_FOLDER, '../user_data');
if (!PRESERVE) {
fs.emptyDirSync(TEST_E2E_RUN_FOLDER);
fs.emptyDirSync(NPM_CACHE_FOLDER);
fs.emptyDirSync(CHROME_USER_DATA_FOLDER);
}
fs.copySync(TEST_E2E_SRC_FOLDER, TEST_E2E_RUN_FOLDER);
// always use a new folder as user-data-dir
let nextUserDataDirId = 0;
function getNextUserDataDir() {
const dir = path.resolve(CHROME_USER_DATA_FOLDER, nextUserDataDirId.toString());
nextUserDataDirId++;
fs.emptyDirSync(dir);
return dir;
}
async function main() {
// find packed package
const { globbySync } = await import('globby');
const ORT_COMMON_FOLDER = path.resolve(JS_ROOT_FOLDER, 'common');
const ORT_COMMON_PACKED_FILEPATH_CANDIDATES = globbySync('onnxruntime-common-*.tgz', { cwd: ORT_COMMON_FOLDER });
const PACKAGES_TO_INSTALL = [];
if (ORT_COMMON_PACKED_FILEPATH_CANDIDATES.length === 1) {
PACKAGES_TO_INSTALL.push(path.resolve(ORT_COMMON_FOLDER, ORT_COMMON_PACKED_FILEPATH_CANDIDATES[0]));
} else if (ORT_COMMON_PACKED_FILEPATH_CANDIDATES.length > 1) {
throw new Error('multiple packages found for onnxruntime-common.');
}
const ORT_WEB_FOLDER = path.resolve(JS_ROOT_FOLDER, 'web');
const ORT_WEB_PACKED_FILEPATH_CANDIDATES = globbySync('onnxruntime-web-*.tgz', { cwd: ORT_WEB_FOLDER });
if (ORT_WEB_PACKED_FILEPATH_CANDIDATES.length !== 1) {
throw new Error('cannot find exactly single package for onnxruntime-web.');
}
PACKAGES_TO_INSTALL.push(path.resolve(ORT_WEB_FOLDER, ORT_WEB_PACKED_FILEPATH_CANDIDATES[0]));
// we start here:
if (!PRESERVE) {
// install dev dependencies
await runInShell(`npm install`);
// npm install with "--cache" to install packed packages with an empty cache folder
await runInShell(`npm install --cache "${NPM_CACHE_FOLDER}" ${PACKAGES_TO_INSTALL.map((i) => `"${i}"`).join(' ')}`);
}
// perform exports testing
{
const testExportsMain = require(path.join(TEST_E2E_RUN_FOLDER, './exports/main'));
await testExportsMain(PRESERVE, PACKAGES_TO_INSTALL);
}
// prepare .wasm files for path override testing
prepareWasmPathOverrideFiles();
// Setup the wwwroot folder for hosting .wasm files (for cross-origin testing)
const serverWwwRoot = path.resolve(TEST_E2E_RUN_FOLDER, 'wwwroot');
fs.emptyDirSync(serverWwwRoot);
// prepare ESM loaders
prepareEsmLoaderFiles();
await fs.symlink(
path.resolve(TEST_E2E_RUN_FOLDER, 'node_modules', 'onnxruntime-web', 'dist'),
path.join(serverWwwRoot, 'dist'),
'junction',
);
await fs.symlink(
path.resolve(TEST_E2E_RUN_FOLDER, 'test-wasm-path-override'),
path.join(serverWwwRoot, 'test-wasm-path-override'),
'junction',
);
// start a HTTP server for hosting .wasm files (for cross-origin testing)
const server = startServer(serverWwwRoot, 8081);
// await delay(1000 * 3600); // wait for 1 hour
try {
// test case run in Node.js
await testAllNodejsCases();
// test cases with self-host (ort hosted in same origin)
await testAllBrowserCases({ hostInKarma: true });
// test cases without self-host (ort hosted in different origin)
await testAllBrowserCases({ hostInKarma: false });
// run bundlers
await runInShell(`npm run build`);
// test package consuming test
await testAllBrowserPackagesConsumingCases();
} finally {
// close the server after all tests
await server.close();
}
}
function prepareEsmLoaderFiles() {
const allEsmFiles = [...new Set(BROWSER_TEST_CASES.map((i) => i[3]).filter((i) => i && i.endsWith('.mjs')))];
// self-hosted
fs.emptyDirSync(path.join(TEST_E2E_RUN_FOLDER, 'esm-loaders'));
fs.emptyDirSync(path.join(TEST_E2E_RUN_FOLDER, 'wwwroot', 'esm-loaders'));
allEsmFiles.forEach((i) => {
fs.writeFileSync(
path.join(TEST_E2E_RUN_FOLDER, 'esm-loaders', i),
`import * as x from '../node_modules/onnxruntime-web/dist/${i}'; globalThis.ort = x;`,
);
fs.writeFileSync(
path.join(TEST_E2E_RUN_FOLDER, 'wwwroot', 'esm-loaders', i),
`import * as x from '../dist/${i}'; globalThis.ort = x;`,
);
});
}
function prepareWasmPathOverrideFiles() {
const folder = path.join(TEST_E2E_RUN_FOLDER, 'test-wasm-path-override');
const sourceFile = path.join(
TEST_E2E_RUN_FOLDER,
'node_modules',
'onnxruntime-web',
'dist',
'ort-wasm-simd-threaded',
);
fs.emptyDirSync(folder);
fs.copyFileSync(`${sourceFile}.mjs`, path.join(folder, 'ort-wasm-simd-threaded.mjs'));
fs.copyFileSync(`${sourceFile}.wasm`, path.join(folder, 'ort-wasm-simd-threaded.wasm'));
fs.copyFileSync(`${sourceFile}.mjs`, path.join(folder, 'renamed.mjs'));
fs.copyFileSync(`${sourceFile}.wasm`, path.join(folder, 'renamed.wasm'));
fs.copyFileSync(`${sourceFile}.jsep.mjs`, path.join(folder, 'ort-wasm-simd-threaded.jsep.mjs'));
fs.copyFileSync(`${sourceFile}.jsep.wasm`, path.join(folder, 'ort-wasm-simd-threaded.jsep.wasm'));
fs.copyFileSync(`${sourceFile}.jsep.mjs`, path.join(folder, 'jsep-renamed.mjs'));
fs.copyFileSync(`${sourceFile}.jsep.wasm`, path.join(folder, 'jsep-renamed.wasm'));
}
async function testAllNodejsCases() {
for (const caseName of NODEJS_TEST_CASES) {
await runInShell(`node ./node_modules/mocha/bin/mocha --timeout 10000 ${caseName}`);
}
}
async function testAllBrowserCases({ hostInKarma }) {
for (const [testForSameOrigin, testForCrossOrigin, main, ortMain, args] of BROWSER_TEST_CASES) {
if (hostInKarma && testForSameOrigin) {
await runKarma({ hostInKarma, main, ortMain, args });
await runKarma({ hostInKarma, main, ortMain, args, enableSharedArrayBuffer: true });
}
if (!hostInKarma && testForCrossOrigin) {
await runKarma({ hostInKarma, main, ortMain, args });
await runKarma({ hostInKarma, main, ortMain, args, enableSharedArrayBuffer: true });
}
}
}
async function testAllBrowserPackagesConsumingCases() {
for (const [main, format] of BUNDLER_TEST_CASES) {
await runKarma({ hostInKarma: true, main, ortMain: '', format });
await runKarma({ hostInKarma: true, main, ortMain: '', format, enableSharedArrayBuffer: true });
}
}
async function runKarma({
hostInKarma,
main,
browser = BROWSER,
ortMain = 'ort.min.js',
format = 'iife',
enableSharedArrayBuffer = false,
args = [],
}) {
const selfHostFlag = hostInKarma ? '--self-host' : '';
const argsStr = args.map((i) => `--test-args=${i}`).join(' ');
const formatFlag = `--format=${format}`;
const enableSharedArrayBufferFlag = enableSharedArrayBuffer ? '--enable-shared-array-buffer' : '';
await runInShell(
`npx karma start --single-run --browsers ${browser} ${selfHostFlag} --ort-main=${ortMain} --test-main=${
main
} --user-data=${getNextUserDataDir()} ${argsStr} ${formatFlag} ${enableSharedArrayBufferFlag}`,
);
}
async function runInShell(cmd) {
console.log('===============================================================');
console.log(' Running command in shell:');
console.log(' > ' + cmd);
console.log('===============================================================');
let complete = false;
const childProcess = spawn(cmd, { shell: true, stdio: 'inherit', cwd: TEST_E2E_RUN_FOLDER });
childProcess.on('close', function (code) {
if (code !== 0) {
process.exit(code);
} else {
complete = true;
}
});
while (!complete) {
await delay(100);
}
}
async function delay(ms) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve();
}, ms);
});
}
main();