onnxruntime/js/web/test/training/e2e/run.js
Caroline Zhu 4dbaa73738
[js/web/training] added end-to-end tests (#18700)
## Summary
* following inference's [set-up for end-to-end
tests](https://github.com/microsoft/onnxruntime/tree/main/js/web/test/e2e),
created an end-to-end test runner for training
* this test runner copies testdata from the [trainingapi
folder](https://github.com/microsoft/onnxruntime/tree/main/onnxruntime/test/testdata/training_api)
* then runs two tests (training session with evalModel & optimizer
model, and training session with the minimum options), and tests if the
ORT-web training package encompasses inference
  * these tests check 
    * createTrainingSession
    * runTrainStep
    * runOptimizerStep if applicable
* the parameters methods (getParametersSize, loadParametersBuffer, and
getContiguousParameters)

## TL;DR
*
[`js/web/test/training/e2e/run.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-c1359c4d401f9ba69e937814219cefe5fd11b151a6ffd084c641af3c82e8216c)
is responsible for setting up and running the end to end tests
*
[`js/web/test/training/e2e/common.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-ee5452491b7b2563d175d13d81d10f2323b12b18589aa4c5798962a8b904a4a8)
contains the test function definitions (`testInferenceFunction`,
`testTrainingFunctionMin`, `testTrainingFunctionAll`)

## Flow
* entrypoint: user runs the following command in the terminal: `npm run
test:training:e2e`
*
[`js/web/package.json`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-79275844e75c3c410bb3a71c7f59b2b633e5a3e975c804ffc47220025084da28)
was modified to include an npm script that will run `run.js` which will
run the end to end tests
*
[`js/web/test/training/e2e/run.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-c1359c4d401f9ba69e937814219cefe5fd11b151a6ffd084c641af3c82e8216c)
is responsible for
  * detecting and installing local tarball packages of ORT-web
  * copying training data to the `js/web/training/e2e/data` folder
* starting two Karma processes. Karma is a test runner framework that
simulates testing in the browser.
* In this case, the tests happen in Chrome. We can configure the tests
to run in Edge and other browsers in the future.
* one of these karma processes is self-hosted, meaning it pulls the
ORT-web package from local
* the other karma process is not self-hosted, meaning it pulls the
ORT-web package from another source. In this case, we start an http
server that serves the ORT-web binaries.
*
[`js/web/test/training/e2e/simple-http-server.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-f798ab485f3ec26c299fe5b2923574c9e4b090200ba20d490bbf6c183286993c)
is responsible for starting the HTTP server and serving the ORT binary
files. This code almost identical to the same code in the inference E2E
tests.
*
[`js/web/test/training/e2e/karma.conf.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-436cfe8f670c768a04895bd4a1874a5e033f85e0e2d84941c62ff1f7c30a9f28)
Karma configuration file that specifies what happens when a karma
process is started. The config specifies Mocha as the testing framework,
which will go through all the loaded files and run any tests that exist
*
[`js/web/test/training/e2e/browser-test-wasm.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-13b6155e106dddc7b531ef671186e69b2aadb8a0f4b2f3001db0991567d78221)
File that contains the tests that Mocha will pick up on and run.
* The test functions (such as testInference and testTrainingFunctionAll)
are defined in
[`js/web/test/training/e2e/common.js`](https://github.com/microsoft/onnxruntime/compare/main...carzh:onnxruntime:carzh/training-e2e-runner?expand=1#diff-ee5452491b7b2563d175d13d81d10f2323b12b18589aa4c5798962a8b904a4a8).

## Notes
* I followed the [tests for training
core](b023de0bfc/orttraining/orttraining/test/training_api/core/training_api_tests.cc)
where they randomly generated input for the training session
* E2E tests are triggered by running `npm run test:training:e2e` --
suggestions for alternative script names are appreciated!!!

## Motivation and Context
- adding training bindings for web
2024-01-12 13:33:33 -08:00

138 lines
5.3 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');
// 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-training/ 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-training');
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');
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);
// training data to copy
const ORT_ROOT_FOLDER = path.resolve(JS_ROOT_FOLDER, '..');
const TRAINING_DATA_FOLDER = path.resolve(ORT_ROOT_FOLDER, 'onnxruntime/test/testdata/training_api');
const TRAININGDATA_DEST = path.resolve(TEST_E2E_RUN_FOLDER, 'data');
// 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;
}
// commandline arguments
const BROWSER = minimist(process.argv.slice(2)).browser || 'Chrome_default';
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:
// 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(' ')}`);
// prepare training data
prepareTrainingDataByCopying();
console.log('===============================================================');
console.log("Running self-hosted tests");
console.log('===============================================================');
// test cases with self-host (ort hosted in same origin)
await testAllBrowserCases({hostInKarma: true});
console.log('===============================================================');
console.log("Running not self-hosted tests");
console.log('===============================================================');
// test cases without self-host (ort hosted in same origin)
startServer(path.resolve(TEST_E2E_RUN_FOLDER, 'node_modules', 'onnxruntime-web'));
await testAllBrowserCases({hostInKarma: false});
// no error occurs, exit with code 0
process.exit(0);
}
async function testAllBrowserCases({hostInKarma}) {
await runKarma({hostInKarma, main: './browser-test-wasm.js'});
}
async function runKarma({hostInKarma, main, browser = BROWSER, ortMain = 'ort.training.wasm.min.js'}) {
console.log('===============================================================');
console.log(`Running karma with the following binary: ${ortMain}`);
console.log('===============================================================');
const selfHostFlag = hostInKarma ? '--self-host' : '';
await runInShell(`npx karma start --single-run --browsers ${browser} ${selfHostFlag} --ort-main=${
ortMain} --test-main=${main} --user-data=${getNextUserDataDir()}`);
}
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);
});
}
function prepareTrainingDataByCopying() {
fs.copySync(TRAINING_DATA_FOLDER, TRAININGDATA_DEST);
console.log(`Copied ${TRAINING_DATA_FOLDER} to ${TRAININGDATA_DEST}`);
}
main();