[js/common] add unit tests for onnxruntime-common (#16812)

### Description
"onnxruntime-common" starts to get more and more complicated, so it's a
good idea to add unit tests for it.

Includes the following changes:
- move `mocha` from each subfolder (js/web/, js/node/) to root (js/), so
that it will be installed once and all subfolder can use.
- add folder `test` in js/common/ as root folder for ort-common tests.
- add sub folder `type-tests`. this folder contains a few typescript
source code, which are excluded from the tsconfig.json. they are not
compiled by default. instead, file `type-tests.ts` calls typescript
compiler (tsc) to check for the files under this folder whether the
compilation result is as expected. If tsc compiles a file successfully
when a failure is expected, this is considered an failed test.
- add sub folder `unit-tests`. files under this folder will be compiled
by default. we use default mode of mocha (using `describe()` and `it()`)
to setup test groups and cases.
- update eslint rules accordingly.
This commit is contained in:
Yulong Wang 2023-07-25 14:37:41 -07:00 committed by GitHub
parent 03ce0a5693
commit 53c771f215
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1263 additions and 2061 deletions

View file

@ -5,7 +5,7 @@
module.exports = {
root: true,
ignorePatterns: ['**/*.js', 'ort-schema/', 'node_modules/', 'types/', 'dist/'],
ignorePatterns: ['**/*.js', 'ort-schema/', 'common/test/type-tests/', 'node_modules/', 'types/', 'dist/'],
env: { 'es6': true },
parser: '@typescript-eslint/parser',
parserOptions: { 'project': 'tsconfig.json', 'sourceType': 'module' },
@ -121,6 +121,12 @@ module.exports = {
'jsdoc/check-indentation': 'error',
'jsdoc/newline-after-description': 'error',
}
}, {
files: ['common/test/**/*.ts'],
rules: {
'@typescript-eslint/naming-convention': 'off',
'import/no-extraneous-dependencies': 'off',
}
}, {
files: ['node/script/**/*.ts', 'node/test/**/*.ts', 'web/script/**/*.ts', 'web/test/**/*.ts'], rules: {
'@typescript-eslint/naming-convention': 'off',

View file

@ -2,4 +2,6 @@ node_modules/
dist/
docs/
/test/**/*.js
tsconfig.tsbuildinfo

View file

@ -9,11 +9,12 @@
},
"author": "fs-eire",
"scripts": {
"build:cjs": "npx tsc --module commonjs --outDir ./dist/cjs",
"build:esm": "npx tsc",
"build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
"build:esm": "tsc",
"build:bundles": "webpack",
"build": "node ./build.js",
"prepare": "npm run build"
"prepare": "npm run build && tsc -p test",
"test": "mocha ./test/**/*.js"
},
"devDependencies": {
"typedoc": "^0.23.22"

View file

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.tools.json",
"exclude": ["type-tests/**/*.ts"],
"compilerOptions": {
"module": "ES2022"
}
}

View file

@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import globby from 'globby';
import assert from 'node:assert';
import {readFileSync} from 'node:fs';
import {dirname, join, normalize, relative} from 'node:path';
import {fileURLToPath} from 'node:url';
import npmlog from 'npmlog';
import typescript from 'typescript';
/**
* @fileoverview
*
* This file is used to run TypeScript type tests.
*
* The type tests are located under `common/test/type-tests/` folder. Each test file is a simple typescrit source file.
* In each test file, any expected failure are marked as comments in the following format:
*
* "// {type-tests}|fail|<line-number-offset>|<error-code>"
*
* - comments should always start with "// {type-tests}|fail|"
* - <line-number-offset> is the line number offset from the comment line
* - <error-code> is the error code of the expected failure
*
* For example:
*
* ```ts
* // {type-tests}|fail|1|2348
* const t0 = ort.Tensor('xxx');
* ```
*
* In this example, the comments indicate that the line number of the expected failure is 1 line after the comment line,
* and the error code is 2348.
*
* The test runner will compile each test file and check if the actual error code matches the expected error code. If
* there is no expected failure, the test runner will check if the test file can be compiled successfully.
*/
// the root folder of type tests
const TYPE_TESTS_DIR = join(dirname(fileURLToPath(import.meta.url)), './type-tests');
/**
* aggregate test files: `*.ts` under folder `common/test/type-tests/`
*
* @returns list of test files
*/
const prepareTestFileList = () =>
//
globby.sync('**/*.ts', {
cwd: TYPE_TESTS_DIR,
absolute: true,
});
/**
* Run typescript compiler on the given files.
*/
const compileTypeScriptFiles = (filepaths: string[]): readonly typescript.Diagnostic[] => {
// TypeScript compiler options, base URL is reset to `TYPE_TESTS_DIR`.
const compilerOptions =
JSON.parse(readFileSync(new URL('./type-tests/tsconfig.json', import.meta.url), 'utf-8')).compilerOptions as
typescript.CompilerOptions;
compilerOptions.baseUrl = TYPE_TESTS_DIR;
// Run TypeScript compiler
const program = typescript.createProgram({
rootNames: filepaths,
options: compilerOptions,
});
return typescript.getPreEmitDiagnostics(program);
};
/**
* Prepare test cases for TypeScript type tests.
* @returns list of test cases. Each test case data contains the test title and a function to run the test.
*/
const prepareTestCases = () => {
npmlog.info('PrepareTestCases', 'Preparing test file lists...');
const testFiles = prepareTestFileList();
npmlog.info('PrepareTestCases', `Preparing test file lists... DONE, ${testFiles.length} file(s) in total.`);
npmlog.info('PrepareTestCases', 'Running TypeScript Compiler...');
const compileResult = compileTypeScriptFiles(testFiles).map(
diagnostic => ({
fileName: normalize(diagnostic.file?.fileName ?? ''),
line: diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start!)?.line ?? -1,
code: diagnostic.code,
}));
npmlog.info('PrepareTestCases', 'Running TypeScript Compiler... DONE.');
npmlog.info('PrepareTestCases', 'Parsing test source files for expected failures...');
const testCases = testFiles.map(filepath => {
const normalizedFilePath = normalize(filepath);
const normalizedRelativePath = normalize(relative(TYPE_TESTS_DIR, filepath));
const fileAllLines = readFileSync(filepath, 'utf-8').split('\n').map(line => line.trim());
const expectedFailures: Array<{line: number; code: number}> = [];
fileAllLines.forEach((line, i) => {
if (line.startsWith('// {type-tests}|fail|')) {
const splitted = line.split('|');
assert(splitted.length === 4, `invalid expected failure comment: ${line}`);
const lineOffset = Number.parseInt(splitted[2], 10);
const code = Number.parseInt(splitted[3], 10);
expectedFailures.push({line: i + lineOffset, code});
}
});
const actualFailures: typeof compileResult = [];
return {filepath: normalizedFilePath, relativePath: normalizedRelativePath, expectedFailures, actualFailures};
});
npmlog.info('PrepareTestCases', 'Parsing test source files for expected failures... DONE.');
// now check if file names is matched
const filePathToTestCaseMap = new Map(testCases.map(testCase => [testCase.filepath, testCase]));
for (const error of compileResult) {
// check file name exists
assert(error.fileName, 'Each compile error should have a file name. Please check TypeScript compiler options.');
// check file name is in test cases
const testCase = filePathToTestCaseMap.get(error.fileName);
assert(testCase, `unexpected error file name: ${error.fileName}`);
testCase.actualFailures.push(error);
}
return testCases.map(testCase => {
const {relativePath, expectedFailures, actualFailures} = testCase;
const testFunction = () => {
if (expectedFailures.length === 0) {
assert.equal(actualFailures.length, 0, `expected to pass but failed: ${JSON.stringify(actualFailures)}`);
} else {
actualFailures.forEach(error => {
const {line, code} = error;
const foundIndex = expectedFailures.findIndex(f => f.line === line && f.code === code);
assert.notEqual(foundIndex, -1, `unexpected failure: line=${line}, code=${code}`);
expectedFailures.splice(foundIndex, 1);
});
assert.equal(expectedFailures.length, 0, `expected to fail but passed: ${JSON.stringify(expectedFailures)}`);
}
};
return {title: relativePath, testBody: testFunction};
});
};
describe('TypeScript type tests', () => {
for (const {title, testBody} of prepareTestCases()) {
it(title, testBody);
}
});

View file

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import {Tensor, TypedTensor} from 'onnxruntime-common';
// construct from type, data (boolean array) and shape (number array)
//
// {type-tests}|pass
new Tensor('bool', [true, true, false, false], [2, 2]) as TypedTensor<'bool'>;
// construct from type and data (boolean array)
//
// {type-tests}|pass
new Tensor('bool', [true, true, false, false]) as TypedTensor<'bool'>;
// construct from type, data (Uint8Array) and shape (number array)
//
// {type-tests}|pass
new Tensor('bool', new Uint8Array([1, 1, 0, 0]), [2, 2]) as TypedTensor<'bool'>;
// construct from type and data (Uint8Array)
//
// {type-tests}|pass
new Tensor('bool', new Uint8Array([1, 1, 0, 0])) as TypedTensor<'bool'>;
// construct from data (boolean array)
//
// {type-tests}|pass
new Tensor([true, true, false, false]) as TypedTensor<'bool'>;
// construct from data (Uint8Array) - type is inferred as 'uint8'
//
// "Conversion of type 'TypedTensor<"uint8">' to type 'TypedTensor<"bool">' may be a mistake because ..."
//
// {type-tests}|fail|1|2352
new Tensor(new Uint8Array([1, 1, 0, 0])) as TypedTensor<'bool'>;

View file

@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import {Tensor, TypedTensor} from 'onnxruntime-common';
// construct from type, data (number array) and shape (number array)
//
// {type-tests}|pass
new Tensor('float32', [1, 2, 3, 4], [2, 2]) as TypedTensor<'float32'>;
// construct from type and data (number array)
//
// {type-tests}|pass
new Tensor('float32', [1, 2, 3, 4]) as TypedTensor<'float32'>;
// construct from type, data (Float32Array) and shape (number array)
//
// {type-tests}|pass
new Tensor('float32', new Float32Array([1, 2, 3, 4]), [2, 2]) as TypedTensor<'float32'>;
// construct from type and data (Float32Array)
//
// {type-tests}|pass
new Tensor('float32', new Float32Array([1, 2, 3, 4])) as TypedTensor<'float32'>;
// construct from data (Float32Array)
//
// {type-tests}|pass
new Tensor(new Float32Array([1, 2, 3, 4])) as TypedTensor<'float32'>;
// construct from data (Float32Array) and shape (number array)
//
// {type-tests}|pass
new Tensor(new Float32Array([1, 2, 3, 4]), [2, 2]) as TypedTensor<'float32'>;
// construct (no params) - need params
//
// "Expected 1-3 arguments, but got 0."
//
// {type-tests}|fail|1|2554
new Tensor();
// construct from type - need data
//
// "No overload matches this call."
//
// {type-tests}|fail|1|2769
new Tensor('float32');
// construct from data (number array) - type cannot be inferred.
//
// "No overload matches this call."
//
// {type-tests}|fail|1|2769
new Tensor([1, 2, 3, 4]);
// construct from data (Float32Array) and shape (number) - type mismatch.
//
// "No overload matches this call."
//
// {type-tests}|fail|1|2769
new Tensor(new Float32Array([1, 2, 3, 4]), 2);

View file

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import {Tensor, TypedTensor} from 'onnxruntime-common';
// construct from type, data (string array) and shape (number array)
//
// {type-tests}|pass
new Tensor('string', ['a', 'b', 'c', 'd'], [2, 2]) as TypedTensor<'string'>;
// construct from type and data (string array)
//
// {type-tests}|pass
new Tensor('string', ['a', 'b', 'c', 'd']) as TypedTensor<'string'>;
// construct from data (string array)
//
// {type-tests}|pass
new Tensor(['a', 'b', 'c', 'd']) as TypedTensor<'string'>;

View file

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as ort from 'onnxruntime-common';
// calling ort.Tensor() - creating tensor without 'new' is not allowed.
//
// "Value of type 'TensorConstructor & TensorFactory' is not callable. Did you mean to include 'new'?"
//
// {type-tests}|fail|1|2348
ort.Tensor('float32', [1, 2, 3, 4], [2, 2]);

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "ES2020",
"target": "ES2020",
"baseUrl": ".",
"paths": {
"onnxruntime-common": ["../../lib/index.ts"]
},
"strict": true
}
}

View file

@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import assert from 'assert/strict';
import {Tensor} from 'onnxruntime-common';
/**
* A list of numerical types that are compatible with JavaScript 'number' value.
*
* 3 elements in each list are:
* - type: a string representing the type name,
* - typedArrayConstructor: the built-in typed array constructor for the type,
* - canBeInferredFromType: whether the type can be inferred from the type name.
*/
export const NUMBER_COMPATIBLE_NUMERICAL_TYPES = [
['int8', Int8Array, true] as const,
['uint8', Uint8Array, true] as const,
['int16', Int16Array, true] as const,
['uint16', Uint16Array, true] as const,
['int32', Int32Array, true] as const,
['uint32', Uint32Array, true] as const,
['float32', Float32Array, true] as const,
['float64', Float64Array, true] as const,
];
/**
* Big integer types
*/
export const BIGINT_TYPES = [
['int64', BigInt64Array, true] as const,
['uint64', BigUint64Array, true] as const,
];
/**
* float16 type, data represented by Uint16Array
*/
export const FLOAT16_TYPE = ['float16', Uint16Array, false] as const;
/**
* A list of all numerical types.
*
* not including string and bool.
*/
export const ALL_NUMERICAL_TYPES = [...NUMBER_COMPATIBLE_NUMERICAL_TYPES, ...BIGINT_TYPES, FLOAT16_TYPE];
/**
* a helper function to assert that a value is an array of a certain type
*/
export const assertIsArrayOf = (value: unknown, type: 'string'|'number'|'boolean'): void => {
assert(Array.isArray(value), 'array should be an array');
for (let i = 0; i < value.length; i++) {
assert.equal(typeof value[i], type, `array should be an array of ${type}s`);
}
};
/**
* the 'TensorAny' is a type allows skip typescript type checking for Tensor.
*
* This allows to write test code to pass invalid parameters to Tensor constructor and check the behavior.
*/
export const TensorAny = Tensor as unknown as {new (...args: unknown[]): Tensor};

View file

@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import assert from 'assert/strict';
import {Tensor} from 'onnxruntime-common';
import {ALL_NUMERICAL_TYPES, assertIsArrayOf, BIGINT_TYPES, NUMBER_COMPATIBLE_NUMERICAL_TYPES, TensorAny} from '../common.js';
describe('Tensor Constructor Tests - check types', () => {
for (const [type, typedArrayConstructor, canBeInferredFromType] of ALL_NUMERICAL_TYPES) {
it(`[${type}] new Tensor(type, typedArray, dims): "tensor.type" should match type passed in`, () => {
const tensor = new Tensor(type, new typedArrayConstructor(4), [2, 2]);
assert.equal(tensor.type, type, `tensor.type should be '${type}'`);
});
it(`[${type}] new Tensor(type, typedArray, dims): "tensor.data" should be instance of expected typed array`, () => {
const tensor = new Tensor(type, new typedArrayConstructor(4), [2, 2]);
assert(
tensor.data instanceof typedArrayConstructor,
`tensor.data should be an instance of '${typedArrayConstructor.name}'`);
});
if (canBeInferredFromType) {
it(`[${type}] new Tensor(typedArray, dims): "tensor.type" should match inferred type`, () => {
const tensor = new Tensor(new typedArrayConstructor(4), [2, 2]);
assert.equal(tensor.type, type, `tensor.type should be '${type}'`);
});
}
it(`[${type}] new Tensor(type, {}, dims): expect to throw because data is invalid`, () => {
assert.throws(() => new TensorAny(type, {}, [2, 2]), TypeError);
});
it(`[${type}] new Tensor(type, arrayBuffer, dims): expect to throw because data is invalid`, () => {
assert.throws(() => new TensorAny(type, new ArrayBuffer(100), [2, 2]), TypeError);
});
}
for (const [type, ] of NUMBER_COMPATIBLE_NUMERICAL_TYPES) {
it(`[${type}] new Tensor(type, numbers, dims): tensor can be constructed from number array`, () => {
const tensor = new Tensor(type, [1, 2, 3, 4], [2, 2]);
assert.equal(tensor.type, type, `tensor.type should be '${type}'`);
});
}
for (const [type, ] of BIGINT_TYPES) {
it(`[${type}] new Tensor(type, numbers, dims): tensor can be constructed from number array`, () => {
const tensor = new Tensor(type, [1, 2, 3, 4], [2, 2]);
assert.equal(tensor.type, type, `tensor.type should be '${type}'`);
});
it(`[${type}] new Tensor(type, bigints, dims): tensor can be constructed from bigint array`, () => {
const tensor = new Tensor(type, [1n, 2n, 3n, 4n], [2, 2]);
assert.equal(tensor.type, type, `tensor.type should be '${type}'`);
});
}
it('[string] new Tensor(\'string\', strings, dims): "tensor.type" should match type passed in', () => {
const tensor = new Tensor('string', ['a', 'b', 'c', 'd'], [2, 2]);
assert.equal(tensor.type, 'string', 'tensor.type should be \'string\'');
});
it('[string] new Tensor(strings, dims): "tensor.data" should match inferred type', () => {
const tensor = new Tensor(['a', 'b', 'c', 'd'], [2, 2]);
assert.equal(tensor.type, 'string', 'tensor.type should be \'string\'');
});
it('[string] new Tensor(\'string\', strings, dims): "tensor.data" should be a string array', () => {
const tensor = new Tensor('string', ['a', 'b', 'c', 'd'], [2, 2]);
assertIsArrayOf(tensor.data, 'string');
});
it('[bool] new Tensor(\'bool\', booleans, dims): "tensor.type" should match type passed in', () => {
const tensor = new Tensor('bool', [true, false, true, false], [2, 2]);
assert.equal(tensor.type, 'bool', 'tensor.type should be \'bool\'');
});
it('[bool] new Tensor(\'bool\', uint8Array, dims): tensor can be constructed from Uint8Array', () => {
const tensor = new Tensor('bool', new Uint8Array([1, 0, 1, 0]), [2, 2]);
assert.equal(tensor.type, 'bool', 'tensor.type should be \'bool\'');
});
it('[bool] new Tensor(booleans, dims): "tensor.data" should match inferred type', () => {
const tensor = new Tensor([true, false, true, false], [2, 2]);
assert.equal(tensor.type, 'bool', 'tensor.type should be \'bool\'');
});
it('[bool] new Tensor(\'bool\', booleans, dims): "tensor.data" should be a boolean array', () => {
const tensor = new Tensor('bool', [true, false, true, false], [2, 2]);
assert(tensor.data instanceof Uint8Array, 'tensor.data should be an instance of \'Uint8Array\'');
});
it('[float16] new Tensor(\'float16\', numbers, dims): ' +
'expect to throw because it\'s not allowed to construct \'float16\' tensor from number array',
() => {
assert.throws(() => new Tensor('float16', [1, 2, 3, 4], [2, 2]), TypeError);
});
it('[badtype] new Tensor(\'a\', numbers, dims): expect to throw because \'a\' is an invalid type', () => {
assert.throws(() => new TensorAny('a', [1, 2, 3, 4], [2, 2]), TypeError);
});
});

1226
js/node/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,11 +35,9 @@
],
"devDependencies": {
"@types/minimist": "^1.2.2",
"@types/mocha": "^10.0.1",
"cmake-js": "^7.2.1",
"jsonc": "^2.0.0",
"minimist": "^1.2.8",
"mocha": "^10.2.0",
"node-addon-api": "^6.0.0",
"onnx-proto": "^8.0.1"
},

786
js/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
{
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/mocha": "^10.0.1",
"@types/node": "^18.14.6",
"@types/npmlog": "^4.1.4",
"@typescript-eslint/eslint-plugin": "^5.54.1",
@ -15,6 +16,7 @@
"eslint-plugin-unicorn": "^46.0.0",
"fs-extra": "^11.1.0",
"jszip": "^3.10.1",
"mocha": "^10.2.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"npmlog": "^7.0.1",
"terser": "^5.16.5",

826
js/web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,6 @@
"@types/karma": "^6.1.0",
"@types/minimatch": "^5.1.2",
"@types/minimist": "^1.2.2",
"@types/mocha": "^10.0.1",
"@types/platform": "^1.3.4",
"@webgpu/types": "^0.1.30",
"base64-js": "^1.5.1",
@ -61,7 +60,6 @@
"karma-sourcemap-loader": "^0.4.0",
"minimatch": "^7.4.2",
"minimist": "^1.2.8",
"mocha": "^10.2.0",
"numpy-parser": "^1.2.3",
"strip-json-comments": "^5.0.0"
},

View file

@ -5,7 +5,7 @@
"downlevelIteration": true,
"declaration": true,
"declarationDir": "./types",
"typeRoots": ["./node_modules/@webgpu/types", "./node_modules/@types"]
"typeRoots": ["./node_modules/@webgpu/types", "./node_modules/@types", "../node_modules/@types"]
},
"include": ["lib", "script", "test"],
"exclude": ["lib/wasm/proxy-worker"]