summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webnn/resources/utils_validation.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webnn/resources/utils_validation.js')
-rw-r--r--testing/web-platform/tests/webnn/resources/utils_validation.js359
1 files changed, 359 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webnn/resources/utils_validation.js b/testing/web-platform/tests/webnn/resources/utils_validation.js
new file mode 100644
index 0000000000..7f1d4a4a94
--- /dev/null
+++ b/testing/web-platform/tests/webnn/resources/utils_validation.js
@@ -0,0 +1,359 @@
+'use strict';
+
+// https://webmachinelearning.github.io/webnn/#enumdef-mloperanddatatype
+const allWebNNOperandDataTypes = [
+ 'float32',
+ 'float16',
+ 'int32',
+ 'uint32',
+ 'int64',
+ 'uint64',
+ 'int8',
+ 'uint8'
+];
+
+const unsignedLongType = 'unsigned long';
+
+const dimensions0D = [];
+const dimensions1D = [2];
+const dimensions2D = [2, 3];
+const dimensions3D = [2, 3, 4];
+const dimensions4D = [2, 3, 4, 5];
+const dimensions5D = [2, 3, 4, 5, 6];
+
+const adjustOffsetsArray = [
+ // Decrease 1
+ -1,
+ // Increase 1
+ 1
+];
+
+// TODO
+// Add more 5+ dimensions
+const allWebNNDimensionsArray = [
+ dimensions0D,
+ dimensions1D,
+ dimensions2D,
+ dimensions3D,
+ dimensions4D,
+ dimensions5D
+];
+
+const notUnsignedLongAxisArray = [
+ // String
+ 'abc',
+ // BigInt
+ BigInt(100),
+ // Object
+ {
+ value: 1
+ },
+ // Array Object
+ [0, 1],
+ // Date Object
+ new Date("2024-01-01"),
+];
+
+function getRank(inputDimensions) {
+ return inputDimensions.length;
+}
+
+function getAxisArray(inputDimensions) {
+ return Array.from({length: inputDimensions.length}, (_, i) => i);
+}
+
+function getAxesArrayContainSameValues(inputDimensions) {
+ // TODO
+ // Currently this function returns an array containing each element which all have the same value.
+ // For example axes: [0, 1, 2] for 3D input tensor
+ // this function returns
+ // [
+ // // two values are same
+ // [0, 0],
+ // [1, 1],
+ // [2, 2],
+ // // three values are same
+ // [0, 0, 0],
+ // [1, 1, 1]
+ // [2, 2, 2]
+ // ]
+ // while it should return
+ // [
+ // // two values are same
+ // [0, 0],
+ // [1, 1],
+ // [2, 2],
+ // [0, 0, 1],
+ // [0, 0, 2],
+ // [0, 1, 0],
+ // [0, 2, 0],
+ // [1, 0, 0],
+ // [2, 0, 0],
+ // [1, 1, 0],
+ // [1, 1, 2],
+ // [1, 0, 1],
+ // [1, 2, 1],
+ // [0, 1, 1],
+ // [2, 1, 1],
+ // [2, 2, 0],
+ // [2, 2, 1],
+ // [2, 0, 2],
+ // [2, 1, 2],
+ // [0, 2, 2],
+ // [1, 2, 2],
+ // // three (all) values are same
+ // [0, 0, 0],
+ // [1, 1, 1]
+ // [2, 2, 2]
+ // ]
+ const axesArrayContainSameValues = [];
+ const length = inputDimensions.length;
+ if (length >= 2) {
+ const validAxesArrayFull = getAxisArray(inputDimensions);
+ for (let index = 0; index < length; index++) {
+ axesArrayContainSameValues.push(new Array(2).fill(validAxesArrayFull[index]));
+ if (length > 2) {
+ axesArrayContainSameValues.push(new Array(3).fill(validAxesArrayFull[index]));
+ }
+ }
+ }
+ return axesArrayContainSameValues;
+}
+
+function generateUnbroadcastableDimensionsArray(dimensions) {
+ // Currently this function returns an array of some unbroadcastable dimensions.
+ // for example given dimensions [2, 3, 4]
+ // this function returns
+ // [
+ // [3, 3, 4],
+ // [2, 2, 4],
+ // [2, 4, 4],
+ // [2, 3, 3],
+ // [2, 3, 5],
+ // [3],
+ // [5],
+ // [1, 3],
+ // [1, 5],
+ // [1, 1, 3],
+ // [1, 1, 5],
+ // [1, 1, 1, 3],
+ // [1, 1, 1, 5],
+ // ]
+ if (dimensions.every(v => v === 1)) {
+ throw new Error(`[${dimensions}] always can be broadcasted`);
+ }
+ const resultDimensions = [];
+ const length = dimensions.length;
+ if (!dimensions.slice(0, length - 1).every(v => v === 1)) {
+ for (let i = 0; i < length; i++) {
+ if (dimensions[i] !== 1) {
+ for (let offset of [-1, 1]) {
+ const dimensionsB = dimensions.slice();
+ dimensionsB[i] += offset;
+ if (dimensionsB[i] !== 1) {
+ resultDimensions.push(dimensionsB);
+ }
+ }
+ }
+ }
+ }
+ const lastDimensionSize = dimensions[length - 1];
+ if (lastDimensionSize !== 1) {
+ for (let j = 0; j <= length; j++) {
+ if (lastDimensionSize > 2) {
+ resultDimensions.push(Array(j).fill(1).concat([lastDimensionSize - 1]));
+ }
+ resultDimensions.push(Array(j).fill(1).concat([lastDimensionSize + 1]));
+ }
+ }
+ return resultDimensions;
+}
+
+function generateOutOfRangeValuesArray(type) {
+ let range, outsideValueArray;
+ switch (type) {
+ case 'unsigned long':
+ // https://webidl.spec.whatwg.org/#idl-unsigned-long
+ // The unsigned long type is an unsigned integer type that has values in the range [0, 4294967295].
+ range = [0, 4294967295];
+ break;
+ default:
+ throw new Error(`Unsupport ${type}`);
+ }
+ outsideValueArray = [range[0] - 1, range[1] + 1];
+ return outsideValueArray;
+}
+
+let inputIndex = 0;
+let inputAIndex = 0;
+let inputBIndex = 0;
+let context, builder;
+
+test(() => assert_not_equals(navigator.ml, undefined, "ml property is defined on navigator"));
+
+promise_setup(async () => {
+ if (navigator.ml === undefined) {
+ return;
+ }
+ context = await navigator.ml.createContext();
+ builder = new MLGraphBuilder(context);
+}, {explicit_timeout: true});
+
+function validateTwoInputsBroadcastable(operationName) {
+ if (navigator.ml === undefined) {
+ return;
+ }
+ promise_test(async t => {
+ for (let dataType of allWebNNOperandDataTypes) {
+ for (let dimensions of allWebNNDimensionsArray) {
+ if (dimensions.length > 0) {
+ const inputA = builder.input(`inputA${++inputAIndex}`, {dataType, dimensions});
+ const unbroadcastableDimensionsArray = generateUnbroadcastableDimensionsArray(dimensions);
+ for (let unbroadcastableDimensions of unbroadcastableDimensionsArray) {
+ const inputB = builder.input(`inputB${++inputBIndex}`, {dataType, dimensions: unbroadcastableDimensions});
+ assert_throws_dom('DataError', () => builder[operationName](inputA, inputB));
+ assert_throws_dom('DataError', () => builder[operationName](inputB, inputA));
+ }
+ }
+ }
+ }
+ }, `[${operationName}] DataError is expected if two inputs aren't broadcastable`);
+}
+
+function validateTwoInputsOfSameDataType(operationName) {
+ if (navigator.ml === undefined) {
+ return;
+ }
+ let operationNameArray;
+ if (typeof operationName === 'string') {
+ operationNameArray = [operationName];
+ } else if (Array.isArray(operationName)) {
+ operationNameArray = operationName;
+ } else {
+ throw new Error(`${operationName} should be an operation name string or an operation name string array`);
+ }
+ for (let subOperationName of operationNameArray) {
+ promise_test(async t => {
+ for (let dataType of allWebNNOperandDataTypes) {
+ for (let dimensions of allWebNNDimensionsArray) {
+ const inputA = builder.input(`inputA${++inputAIndex}`, {dataType, dimensions});
+ for (let dataTypeB of allWebNNOperandDataTypes) {
+ if (dataType !== dataTypeB) {
+ const inputB = builder.input(`inputB${++inputBIndex}`, {dataType: dataTypeB, dimensions});
+ assert_throws_dom('DataError', () => builder[subOperationName](inputA, inputB));
+ }
+ }
+ }
+ }
+ }, `[${subOperationName}] DataError is expected if two inputs aren't of same data type`);
+ }
+}
+
+/**
+ * Validate options.axes by given operation and input rank for
+ * argMin/Max / layerNormalization / Reduction operations / resample2d operations
+ * @param {(String[]|String)} operationName - An operation name array or an operation name
+ * @param {Number} [inputRank]
+ */
+function validateOptionsAxes(operationName, inputRank) {
+ if (navigator.ml === undefined) {
+ return;
+ }
+ let operationNameArray;
+ if (typeof operationName === 'string') {
+ operationNameArray = [operationName];
+ } else if (Array.isArray(operationName)) {
+ operationNameArray = operationName;
+ } else {
+ throw new Error(`${operationName} should be an operation name string or an operation name string array`);
+ }
+ const invalidAxisArray = generateOutOfRangeValuesArray(unsignedLongType);
+ for (let subOperationName of operationNameArray) {
+ // TypeError is expected if any of options.axes elements is not an unsigned long interger
+ promise_test(async t => {
+ if (inputRank === undefined) {
+ // argMin/Max / layerNormalization / Reduction operations
+ for (let dataType of allWebNNOperandDataTypes) {
+ for (let dimensions of allWebNNDimensionsArray) {
+ const rank = getRank(dimensions);
+ if (rank >= 1) {
+ const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
+ for (let invalidAxis of invalidAxisArray) {
+ assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: invalidAxis}));
+ }
+ for (let axis of notUnsignedLongAxisArray) {
+ assert_false(typeof axis === 'number' && Number.isInteger(axis), `[${subOperationName}] any of options.axes elements should be of 'unsigned long'`);
+ assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: [axis]}));
+ }
+ }
+ }
+ }
+ } else {
+ // resample2d
+ for (let dataType of allWebNNOperandDataTypes) {
+ const input = builder.input(`input${++inputIndex}`, {dataType, dimensions: allWebNNDimensionsArray[inputRank]});
+ for (let invalidAxis of invalidAxisArray) {
+ assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: invalidAxis}));
+ }
+ for (let axis of notUnsignedLongAxisArray) {
+ assert_false(typeof axis === 'number' && Number.isInteger(axis), `[${subOperationName}] any of options.axes elements should be of 'unsigned long'`);
+ assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: [axis]}));
+ }
+ }
+ }
+ }, `[${subOperationName}] TypeError is expected if any of options.axes elements is not an unsigned long interger`);
+
+ // DataError is expected if any of options.axes elements is greater or equal to the size of input
+ promise_test(async t => {
+ if (inputRank === undefined) {
+ // argMin/Max / layerNormalization / Reduction operations
+ for (let dataType of allWebNNOperandDataTypes) {
+ for (let dimensions of allWebNNDimensionsArray) {
+ const rank = getRank(dimensions);
+ if (rank >= 1) {
+ const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
+ assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [rank]}));
+ assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [rank + 1]}));
+ }
+ }
+ }
+ } else {
+ // resample2d
+ for (let dataType of allWebNNOperandDataTypes) {
+ const input = builder.input(`input${++inputIndex}`, {dataType, dimensions: allWebNNDimensionsArray[inputRank]});
+ assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [inputRank]}));
+ assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [inputRank + 1]}));
+ }
+ }
+ }, `[${subOperationName}] DataError is expected if any of options.axes elements is greater or equal to the size of input`);
+
+ // DataError is expected if two or more values are same in the axes sequence
+ promise_test(async t => {
+ if (inputRank === undefined) {
+ // argMin/Max / layerNormalization / Reduction operations
+ for (let dataType of allWebNNOperandDataTypes) {
+ for (let dimensions of allWebNNDimensionsArray) {
+ const rank = getRank(dimensions);
+ if (rank >= 2) {
+ const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
+ const axesArrayContainSameValues = getAxesArrayContainSameValues(dimensions);
+ for (let axes of axesArrayContainSameValues) {
+ assert_throws_dom('DataError', () => builder[subOperationName](input, {axes}));
+ }
+ }
+ }
+ }
+ } else {
+ // resample2d
+ for (let dataType of allWebNNOperandDataTypes) {
+ const dimensions = allWebNNDimensionsArray[inputRank];
+ const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
+ const axesArrayContainSameValues = getAxesArrayContainSameValues(dimensions);
+ for (let axes of axesArrayContainSameValues) {
+ assert_throws_dom('DataError', () => builder[subOperationName](input, {axes}));
+ }
+ }
+ }
+ }, `[${subOperationName}] DataError is expected if two or more values are same in the axes sequence`);
+ }
+}