diff options
Diffstat (limited to 'testing/web-platform/tests/webaudio/resources/audit.js')
-rw-r--r-- | testing/web-platform/tests/webaudio/resources/audit.js | 1445 |
1 files changed, 1445 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/resources/audit.js b/testing/web-platform/tests/webaudio/resources/audit.js new file mode 100644 index 0000000000..2bb078b111 --- /dev/null +++ b/testing/web-platform/tests/webaudio/resources/audit.js @@ -0,0 +1,1445 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// See https://github.com/web-platform-tests/wpt/issues/12781 for information on +// the purpose of audit.js, and why testharness.js does not suffice. + +/** + * @fileOverview WebAudio layout test utility library. Built around W3C's + * testharness.js. Includes asynchronous test task manager, + * assertion utilities. + * @dependency testharness.js + */ + + +(function() { + + 'use strict'; + + // Selected methods from testharness.js. + let testharnessProperties = [ + 'test', 'async_test', 'promise_test', 'promise_rejects_js', 'generate_tests', + 'setup', 'done', 'assert_true', 'assert_false' + ]; + + // Check if testharness.js is properly loaded. Throw otherwise. + for (let name in testharnessProperties) { + if (!self.hasOwnProperty(testharnessProperties[name])) + throw new Error('Cannot proceed. testharness.js is not loaded.'); + } +})(); + + +window.Audit = (function() { + + 'use strict'; + + // NOTE: Moving this method (or any other code above) will change the location + // of 'CONSOLE ERROR...' message in the expected text files. + function _logError(message) { + console.error('[audit.js] ' + message); + } + + function _logPassed(message) { + test(function(arg) { + assert_true(true); + }, message); + } + + function _logFailed(message, detail) { + test(function() { + assert_true(false, detail); + }, message); + } + + function _throwException(message) { + throw new Error(message); + } + + // TODO(hongchan): remove this hack after confirming all the tests are + // finished correctly. (crbug.com/708817) + const _testharnessDone = window.done; + window.done = () => { + _throwException('Do NOT call done() method from the test code.'); + }; + + // Generate a descriptive string from a target value in various types. + function _generateDescription(target, options) { + let targetString; + + switch (typeof target) { + case 'object': + // Handle Arrays. + if (target instanceof Array || target instanceof Float32Array || + target instanceof Float64Array || target instanceof Uint8Array) { + let arrayElements = target.length < options.numberOfArrayElements ? + String(target) : + String(target.slice(0, options.numberOfArrayElements)) + '...'; + targetString = '[' + arrayElements + ']'; + } else if (target === null) { + targetString = String(target); + } else { + targetString = '' + String(target).split(/[\s\]]/)[1]; + } + break; + case 'function': + if (Error.isPrototypeOf(target)) { + targetString = "EcmaScript error " + target.name; + } else { + targetString = String(target); + } + break; + default: + targetString = String(target); + break; + } + + return targetString; + } + + // Return a string suitable for printing one failed element in + // |beCloseToArray|. + function _formatFailureEntry(index, actual, expected, abserr, threshold) { + return '\t[' + index + ']\t' + actual.toExponential(16) + '\t' + + expected.toExponential(16) + '\t' + abserr.toExponential(16) + '\t' + + (abserr / Math.abs(expected)).toExponential(16) + '\t' + + threshold.toExponential(16); + } + + // Compute the error threshold criterion for |beCloseToArray| + function _closeToThreshold(abserr, relerr, expected) { + return Math.max(abserr, relerr * Math.abs(expected)); + } + + /** + * @class Should + * @description Assertion subtask for the Audit task. + * @param {Task} parentTask Associated Task object. + * @param {Any} actual Target value to be tested. + * @param {String} actualDescription String description of the test target. + */ + class Should { + constructor(parentTask, actual, actualDescription) { + this._task = parentTask; + + this._actual = actual; + this._actualDescription = (actualDescription || null); + this._expected = null; + this._expectedDescription = null; + + this._detail = ''; + // If true and the test failed, print the actual value at the + // end of the message. + this._printActualForFailure = true; + + this._result = null; + + /** + * @param {Number} numberOfErrors Number of errors to be printed. + * @param {Number} numberOfArrayElements Number of array elements to be + * printed in the test log. + * @param {Boolean} verbose Verbose output from the assertion. + */ + this._options = { + numberOfErrors: 4, + numberOfArrayElements: 16, + verbose: false + }; + } + + _processArguments(args) { + if (args.length === 0) + return; + + if (args.length > 0) + this._expected = args[0]; + + if (typeof args[1] === 'string') { + // case 1: (expected, description, options) + this._expectedDescription = args[1]; + Object.assign(this._options, args[2]); + } else if (typeof args[1] === 'object') { + // case 2: (expected, options) + Object.assign(this._options, args[1]); + } + } + + _buildResultText() { + if (this._result === null) + _throwException('Illegal invocation: the assertion is not finished.'); + + let actualString = _generateDescription(this._actual, this._options); + + // Use generated text when the description is not provided. + if (!this._actualDescription) + this._actualDescription = actualString; + + if (!this._expectedDescription) { + this._expectedDescription = + _generateDescription(this._expected, this._options); + } + + // For the assertion with a single operand. + this._detail = + this._detail.replace(/\$\{actual\}/g, this._actualDescription); + + // If there is a second operand (i.e. expected value), we have to build + // the string for it as well. + this._detail = + this._detail.replace(/\$\{expected\}/g, this._expectedDescription); + + // If there is any property in |_options|, replace the property name + // with the value. + for (let name in this._options) { + if (name === 'numberOfErrors' || name === 'numberOfArrayElements' || + name === 'verbose') { + continue; + } + + // The RegExp key string contains special character. Take care of it. + let re = '\$\{' + name + '\}'; + re = re.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); + this._detail = this._detail.replace( + new RegExp(re, 'g'), _generateDescription(this._options[name])); + } + + // If the test failed, add the actual value at the end. + if (this._result === false && this._printActualForFailure === true) { + this._detail += ' Got ' + actualString + '.'; + } + } + + _finalize() { + if (this._result) { + _logPassed(' ' + this._detail); + } else { + _logFailed('X ' + this._detail); + } + + // This assertion is finished, so update the parent task accordingly. + this._task.update(this); + + // TODO(hongchan): configurable 'detail' message. + } + + _assert(condition, passDetail, failDetail) { + this._result = Boolean(condition); + this._detail = this._result ? passDetail : failDetail; + this._buildResultText(); + this._finalize(); + + return this._result; + } + + get result() { + return this._result; + } + + get detail() { + return this._detail; + } + + /** + * should() assertions. + * + * @example All the assertions can have 1, 2 or 3 arguments: + * should().doAssert(expected); + * should().doAssert(expected, options); + * should().doAssert(expected, expectedDescription, options); + * + * @param {Any} expected Expected value of the assertion. + * @param {String} expectedDescription Description of expected value. + * @param {Object} options Options for assertion. + * @param {Number} options.numberOfErrors Number of errors to be printed. + * (if applicable) + * @param {Number} options.numberOfArrayElements Number of array elements + * to be printed. (if + * applicable) + * @notes Some assertions can have additional options for their specific + * testing. + */ + + /** + * Check if |actual| exists. + * + * @example + * should({}, 'An empty object').exist(); + * @result + * "PASS An empty object does exist." + */ + exist() { + return this._assert( + this._actual !== null && this._actual !== undefined, + '${actual} does exist.', '${actual} does not exist.'); + } + + /** + * Check if |actual| operation wrapped in a function throws an exception + * with a expected error type correctly. |expected| is optional. If it is an + * instance of DOMException, then the description (second argument) can be + * provided to be more strict about the expected exception type. |expected| + * also can be other generic error types such as TypeError, RangeError or + * etc. + * + * @example + * should(() => { let a = b; }, 'A bad code').throw(); + * should(() => { new SomeConstructor(); }, 'A bad construction') + * .throw(DOMException, 'NotSupportedError'); + * should(() => { let c = d; }, 'Assigning d to c') + * .throw(ReferenceError); + * should(() => { let e = f; }, 'Assigning e to f') + * .throw(ReferenceError, { omitErrorMessage: true }); + * + * @result + * "PASS A bad code threw an exception of ReferenceError: b is not + * defined." + * "PASS A bad construction threw DOMException:NotSupportedError." + * "PASS Assigning d to c threw ReferenceError: d is not defined." + * "PASS Assigning e to f threw ReferenceError: [error message + * omitted]." + */ + throw() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let didThrowCorrectly = false; + let passDetail, failDetail; + + try { + // This should throw. + this._actual(); + // Catch did not happen, so the test is failed. + failDetail = '${actual} did not throw an exception.'; + } catch (error) { + let errorMessage = this._options.omitErrorMessage ? + ': [error message omitted]' : + ': "' + error.message + '"'; + if (this._expected === null || this._expected === undefined) { + // The expected error type was not given. + didThrowCorrectly = true; + passDetail = '${actual} threw ' + error.name + errorMessage + '.'; + } else if (this._expected === DOMException && + this._expectedDescription !== undefined) { + // Handles DOMException with an expected exception name. + if (this._expectedDescription === error.name) { + didThrowCorrectly = true; + passDetail = '${actual} threw ${expected}' + errorMessage + '.'; + } else { + didThrowCorrectly = false; + failDetail = + '${actual} threw "' + error.name + '" instead of ${expected}.'; + } + } else if (this._expected == error.constructor) { + // Handler other error types. + didThrowCorrectly = true; + passDetail = '${actual} threw ' + error.name + errorMessage + '.'; + } else { + didThrowCorrectly = false; + failDetail = + '${actual} threw "' + error.name + '" instead of ${expected}.'; + } + } + + return this._assert(didThrowCorrectly, passDetail, failDetail); + } + + /** + * Check if |actual| operation wrapped in a function does not throws an + * exception correctly. + * + * @example + * should(() => { let foo = 'bar'; }, 'let foo = "bar"').notThrow(); + * + * @result + * "PASS let foo = "bar" did not throw an exception." + */ + notThrow() { + this._printActualForFailure = false; + + let didThrowCorrectly = false; + let passDetail, failDetail; + + try { + this._actual(); + passDetail = '${actual} did not throw an exception.'; + } catch (error) { + didThrowCorrectly = true; + failDetail = '${actual} incorrectly threw ' + error.name + ': "' + + error.message + '".'; + } + + return this._assert(!didThrowCorrectly, passDetail, failDetail); + } + + /** + * Check if |actual| promise is resolved correctly. Note that the returned + * result from promise object will be passed to the following then() + * function. + * + * @example + * should('My promise', promise).beResolve().then((result) => { + * log(result); + * }); + * + * @result + * "PASS My promise resolved correctly." + * "FAIL X My promise rejected *INCORRECTLY* with _ERROR_." + */ + beResolved() { + return this._actual.then( + function(result) { + this._assert(true, '${actual} resolved correctly.', null); + return result; + }.bind(this), + function(error) { + this._assert( + false, null, + '${actual} rejected incorrectly with ' + error + '.'); + }.bind(this)); + } + + /** + * Check if |actual| promise is rejected correctly. + * + * @example + * should('My promise', promise).beRejected().then(nextStuff); + * + * @result + * "PASS My promise rejected correctly (with _ERROR_)." + * "FAIL X My promise resolved *INCORRECTLY*." + */ + beRejected() { + return this._actual.then( + function() { + this._assert(false, null, '${actual} resolved incorrectly.'); + }.bind(this), + function(error) { + this._assert( + true, '${actual} rejected correctly with ' + error + '.', null); + }.bind(this)); + } + + /** + * Check if |actual| promise is rejected correctly. + * + * @example + * should(promise, 'My promise').beRejectedWith('_ERROR_').then(); + * + * @result + * "PASS My promise rejected correctly with _ERROR_." + * "FAIL X My promise rejected correctly but got _ACTUAL_ERROR instead of + * _EXPECTED_ERROR_." + * "FAIL X My promise resolved incorrectly." + */ + beRejectedWith() { + this._processArguments(arguments); + + return this._actual.then( + function() { + this._assert(false, null, '${actual} resolved incorrectly.'); + }.bind(this), + function(error) { + if (this._expected !== error.name) { + this._assert( + false, null, + '${actual} rejected correctly but got ' + error.name + + ' instead of ' + this._expected + '.'); + } else { + this._assert( + true, + '${actual} rejected correctly with ' + this._expected + '.', + null); + } + }.bind(this)); + } + + /** + * Check if |actual| is a boolean true. + * + * @example + * should(3 < 5, '3 < 5').beTrue(); + * + * @result + * "PASS 3 < 5 is true." + */ + beTrue() { + return this._assert( + this._actual === true, '${actual} is true.', + '${actual} is not true.'); + } + + /** + * Check if |actual| is a boolean false. + * + * @example + * should(3 > 5, '3 > 5').beFalse(); + * + * @result + * "PASS 3 > 5 is false." + */ + beFalse() { + return this._assert( + this._actual === false, '${actual} is false.', + '${actual} is not false.'); + } + + /** + * Check if |actual| is strictly equal to |expected|. (no type coercion) + * + * @example + * should(1).beEqualTo(1); + * + * @result + * "PASS 1 is equal to 1." + */ + beEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual === this._expected, '${actual} is equal to ${expected}.', + '${actual} is not equal to ${expected}.'); + } + + /** + * Check if |actual| is not equal to |expected|. + * + * @example + * should(1).notBeEqualTo(2); + * + * @result + * "PASS 1 is not equal to 2." + */ + notBeEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual !== this._expected, + '${actual} is not equal to ${expected}.', + '${actual} should not be equal to ${expected}.'); + } + + /** + * check if |actual| is NaN + * + * @example + * should(NaN).beNaN(); + * + * @result + * "PASS NaN is NaN" + * + */ + beNaN() { + this._processArguments(arguments); + return this._assert( + isNaN(this._actual), + '${actual} is NaN.', + '${actual} is not NaN but should be.'); + } + + /** + * check if |actual| is NOT NaN + * + * @example + * should(42).notBeNaN(); + * + * @result + * "PASS 42 is not NaN" + * + */ + notBeNaN() { + this._processArguments(arguments); + return this._assert( + !isNaN(this._actual), + '${actual} is not NaN.', + '${actual} is NaN but should not be.'); + } + + /** + * Check if |actual| is greater than |expected|. + * + * @example + * should(2).beGreaterThanOrEqualTo(2); + * + * @result + * "PASS 2 is greater than or equal to 2." + */ + beGreaterThan() { + this._processArguments(arguments); + return this._assert( + this._actual > this._expected, + '${actual} is greater than ${expected}.', + '${actual} is not greater than ${expected}.'); + } + + /** + * Check if |actual| is greater than or equal to |expected|. + * + * @example + * should(2).beGreaterThan(1); + * + * @result + * "PASS 2 is greater than 1." + */ + beGreaterThanOrEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual >= this._expected, + '${actual} is greater than or equal to ${expected}.', + '${actual} is not greater than or equal to ${expected}.'); + } + + /** + * Check if |actual| is less than |expected|. + * + * @example + * should(1).beLessThan(2); + * + * @result + * "PASS 1 is less than 2." + */ + beLessThan() { + this._processArguments(arguments); + return this._assert( + this._actual < this._expected, '${actual} is less than ${expected}.', + '${actual} is not less than ${expected}.'); + } + + /** + * Check if |actual| is less than or equal to |expected|. + * + * @example + * should(1).beLessThanOrEqualTo(1); + * + * @result + * "PASS 1 is less than or equal to 1." + */ + beLessThanOrEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual <= this._expected, + '${actual} is less than or equal to ${expected}.', + '${actual} is not less than or equal to ${expected}.'); + } + + /** + * Check if |actual| array is filled with a constant |expected| value. + * + * @example + * should([1, 1, 1]).beConstantValueOf(1); + * + * @result + * "PASS [1,1,1] contains only the constant 1." + */ + beConstantValueOf() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + let errors = {}; + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + if (actual[index] !== expected) + errors[index] = actual[index]; + } + + let numberOfErrors = Object.keys(errors).length; + passed = numberOfErrors === 0; + + if (passed) { + passDetail = '${actual} contains only the constant ${expected}.'; + } else { + let counter = 0; + failDetail = + '${actual}: Expected ${expected} for all values but found ' + + numberOfErrors + ' unexpected values: '; + failDetail += '\n\tIndex\tActual'; + for (let errorIndex in errors) { + failDetail += '\n\t[' + errorIndex + ']' + + '\t' + errors[errorIndex]; + if (++counter >= this._options.numberOfErrors) { + failDetail += + '\n\t...and ' + (numberOfErrors - counter) + ' more errors.'; + break; + } + } + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| array is not filled with a constant |expected| value. + * + * @example + * should([1, 0, 1]).notBeConstantValueOf(1); + * should([0, 0, 0]).notBeConstantValueOf(0); + * + * @result + * "PASS [1,0,1] is not constantly 1 (contains 1 different value)." + * "FAIL X [0,0,0] should have contain at least one value different + * from 0." + */ + notBeConstantValueOf() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail; + let failDetail; + let differences = {}; + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + if (actual[index] !== expected) + differences[index] = actual[index]; + } + + let numberOfDifferences = Object.keys(differences).length; + passed = numberOfDifferences > 0; + + if (passed) { + let valueString = numberOfDifferences > 1 ? 'values' : 'value'; + passDetail = '${actual} is not constantly ${expected} (contains ' + + numberOfDifferences + ' different ' + valueString + ').'; + } else { + failDetail = '${actual} should have contain at least one value ' + + 'different from ${expected}.'; + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| array is identical to |expected| array element-wise. + * + * @example + * should([1, 2, 3]).beEqualToArray([1, 2, 3]); + * + * @result + * "[1,2,3] is identical to the array [1,2,3]." + */ + beEqualToArray() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + let errorIndices = []; + + if (this._actual.length !== this._expected.length) { + passed = false; + failDetail = 'The array length does not match.'; + return this._assert(passed, passDetail, failDetail); + } + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + if (actual[index] !== expected[index]) + errorIndices.push(index); + } + + passed = errorIndices.length === 0; + + if (passed) { + passDetail = '${actual} is identical to the array ${expected}.'; + } else { + let counter = 0; + failDetail = + '${actual} expected to be equal to the array ${expected} ' + + 'but differs in ' + errorIndices.length + ' places:' + + '\n\tIndex\tActual\t\t\tExpected'; + for (let index of errorIndices) { + failDetail += '\n\t[' + index + ']' + + '\t' + this._actual[index].toExponential(16) + '\t' + + this._expected[index].toExponential(16); + if (++counter >= this._options.numberOfErrors) { + failDetail += '\n\t...and ' + (errorIndices.length - counter) + + ' more errors.'; + break; + } + } + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| array contains only the values in |expected| in the + * order of values in |expected|. + * + * @example + * Should([1, 1, 3, 3, 2], 'My random array').containValues([1, 3, 2]); + * + * @result + * "PASS [1,1,3,3,2] contains all the expected values in the correct + * order: [1,3,2]. + */ + containValues() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let indexedActual = []; + let firstErrorIndex = null; + + // Collect the unique value sequence from the actual. + for (let i = 0, prev = null; i < this._actual.length; i++) { + if (this._actual[i] !== prev) { + indexedActual.push({index: i, value: this._actual[i]}); + prev = this._actual[i]; + } + } + + // Compare against the expected sequence. + let failMessage = + '${actual} expected to have the value sequence of ${expected} but ' + + 'got '; + if (this._expected.length === indexedActual.length) { + for (let j = 0; j < this._expected.length; j++) { + if (this._expected[j] !== indexedActual[j].value) { + firstErrorIndex = indexedActual[j].index; + passed = false; + failMessage += this._actual[firstErrorIndex] + ' at index ' + + firstErrorIndex + '.'; + break; + } + } + } else { + passed = false; + let indexedValues = indexedActual.map(x => x.value); + failMessage += `${indexedActual.length} values, [${ + indexedValues}], instead of ${this._expected.length}.`; + } + + return this._assert( + passed, + '${actual} contains all the expected values in the correct order: ' + + '${expected}.', + failMessage); + } + + /** + * Check if |actual| array does not have any glitches. Note that |threshold| + * is not optional and is to define the desired threshold value. + * + * @example + * should([0.5, 0.5, 0.55, 0.5, 0.45, 0.5]).notGlitch(0.06); + * + * @result + * "PASS [0.5,0.5,0.55,0.5,0.45,0.5] has no glitch above the threshold + * of 0.06." + * + */ + notGlitch() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + let diff = Math.abs(actual[index - 1] - actual[index]); + if (diff >= expected) { + passed = false; + failDetail = '${actual} has a glitch at index ' + index + + ' of size ' + diff + '.'; + } + } + + passDetail = + '${actual} has no glitch above the threshold of ${expected}.'; + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| is close to |expected| using the given relative error + * |threshold|. + * + * @example + * should(2.3).beCloseTo(2, { threshold: 0.3 }); + * + * @result + * "PASS 2.3 is 2 within an error of 0.3." + * @param {Object} options Options for assertion. + * @param {Number} options.threshold Threshold value for the comparison. + */ + beCloseTo() { + this._processArguments(arguments); + + // The threshold is relative except when |expected| is zero, in which case + // it is absolute. + let absExpected = this._expected ? Math.abs(this._expected) : 1; + let error = Math.abs(this._actual - this._expected) / absExpected; + + return this._assert( + error <= this._options.threshold, + '${actual} is ${expected} within an error of ${threshold}.', + '${actual} is not close to ${expected} within a relative error of ' + + '${threshold} (RelErr=' + error + ').'); + } + + /** + * Check if |target| array is close to |expected| array element-wise within + * a certain error bound given by the |options|. + * + * The error criterion is: + * abs(actual[k] - expected[k]) < max(absErr, relErr * abs(expected)) + * + * If nothing is given for |options|, then absErr = relErr = 0. If + * absErr = 0, then the error criterion is a relative error. A non-zero + * absErr value produces a mix intended to handle the case where the + * expected value is 0, allowing the target value to differ by absErr from + * the expected. + * + * @param {Number} options.absoluteThreshold Absolute threshold. + * @param {Number} options.relativeThreshold Relative threshold. + */ + beCloseToArray() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + + // Parsing options. + let absErrorThreshold = (this._options.absoluteThreshold || 0); + let relErrorThreshold = (this._options.relativeThreshold || 0); + + // A collection of all of the values that satisfy the error criterion. + // This holds the absolute difference between the target element and the + // expected element. + let errors = {}; + + // Keep track of the max absolute error found. + let maxAbsError = -Infinity, maxAbsErrorIndex = -1; + + // Keep track of the max relative error found, ignoring cases where the + // relative error is Infinity because the expected value is 0. + let maxRelError = -Infinity, maxRelErrorIndex = -1; + + let actual = this._actual; + let expected = this._expected; + + for (let index = 0; index < expected.length; ++index) { + let diff = Math.abs(actual[index] - expected[index]); + let absExpected = Math.abs(expected[index]); + let relError = diff / absExpected; + + if (diff > + Math.max(absErrorThreshold, relErrorThreshold * absExpected)) { + if (diff > maxAbsError) { + maxAbsErrorIndex = index; + maxAbsError = diff; + } + + if (!isNaN(relError) && relError > maxRelError) { + maxRelErrorIndex = index; + maxRelError = relError; + } + + errors[index] = diff; + } + } + + let numberOfErrors = Object.keys(errors).length; + let maxAllowedErrorDetail = JSON.stringify({ + absoluteThreshold: absErrorThreshold, + relativeThreshold: relErrorThreshold + }); + + if (numberOfErrors === 0) { + // The assertion was successful. + passDetail = '${actual} equals ${expected} with an element-wise ' + + 'tolerance of ' + maxAllowedErrorDetail + '.'; + } else { + // Failed. Prepare the detailed failure log. + passed = false; + failDetail = '${actual} does not equal ${expected} with an ' + + 'element-wise tolerance of ' + maxAllowedErrorDetail + '.\n'; + + // Print out actual, expected, absolute error, and relative error. + let counter = 0; + failDetail += '\tIndex\tActual\t\t\tExpected\t\tAbsError' + + '\t\tRelError\t\tTest threshold'; + let printedIndices = []; + for (let index in errors) { + failDetail += + '\n' + + _formatFailureEntry( + index, actual[index], expected[index], errors[index], + _closeToThreshold( + absErrorThreshold, relErrorThreshold, expected[index])); + + printedIndices.push(index); + if (++counter > this._options.numberOfErrors) { + failDetail += + '\n\t...and ' + (numberOfErrors - counter) + ' more errors.'; + break; + } + } + + // Finalize the error log: print out the location of both the maxAbs + // error and the maxRel error so we can adjust thresholds appropriately + // in the test. + failDetail += '\n' + + '\tMax AbsError of ' + maxAbsError.toExponential(16) + + ' at index of ' + maxAbsErrorIndex + '.\n'; + if (printedIndices.find(element => { + return element == maxAbsErrorIndex; + }) === undefined) { + // Print an entry for this index if we haven't already. + failDetail += + _formatFailureEntry( + maxAbsErrorIndex, actual[maxAbsErrorIndex], + expected[maxAbsErrorIndex], errors[maxAbsErrorIndex], + _closeToThreshold( + absErrorThreshold, relErrorThreshold, + expected[maxAbsErrorIndex])) + + '\n'; + } + failDetail += '\tMax RelError of ' + maxRelError.toExponential(16) + + ' at index of ' + maxRelErrorIndex + '.\n'; + if (printedIndices.find(element => { + return element == maxRelErrorIndex; + }) === undefined) { + // Print an entry for this index if we haven't already. + failDetail += + _formatFailureEntry( + maxRelErrorIndex, actual[maxRelErrorIndex], + expected[maxRelErrorIndex], errors[maxRelErrorIndex], + _closeToThreshold( + absErrorThreshold, relErrorThreshold, + expected[maxRelErrorIndex])) + + '\n'; + } + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * A temporary escape hat for printing an in-task message. The description + * for the |actual| is required to get the message printed properly. + * + * TODO(hongchan): remove this method when the transition from the old Audit + * to the new Audit is completed. + * @example + * should(true, 'The message is').message('truthful!', 'false!'); + * + * @result + * "PASS The message is truthful!" + */ + message(passDetail, failDetail) { + return this._assert( + this._actual, '${actual} ' + passDetail, '${actual} ' + failDetail); + } + + /** + * Check if |expected| property is truly owned by |actual| object. + * + * @example + * should(BaseAudioContext.prototype, + * 'BaseAudioContext.prototype').haveOwnProperty('createGain'); + * + * @result + * "PASS BaseAudioContext.prototype has an own property of + * 'createGain'." + */ + haveOwnProperty() { + this._processArguments(arguments); + + return this._assert( + this._actual.hasOwnProperty(this._expected), + '${actual} has an own property of "${expected}".', + '${actual} does not own the property of "${expected}".'); + } + + + /** + * Check if |expected| property is not owned by |actual| object. + * + * @example + * should(BaseAudioContext.prototype, + * 'BaseAudioContext.prototype') + * .notHaveOwnProperty('startRendering'); + * + * @result + * "PASS BaseAudioContext.prototype does not have an own property of + * 'startRendering'." + */ + notHaveOwnProperty() { + this._processArguments(arguments); + + return this._assert( + !this._actual.hasOwnProperty(this._expected), + '${actual} does not have an own property of "${expected}".', + '${actual} has an own the property of "${expected}".') + } + + + /** + * Check if an object is inherited from a class. This looks up the entire + * prototype chain of a given object and tries to find a match. + * + * @example + * should(sourceNode, 'A buffer source node') + * .inheritFrom('AudioScheduledSourceNode'); + * + * @result + * "PASS A buffer source node inherits from 'AudioScheduledSourceNode'." + */ + inheritFrom() { + this._processArguments(arguments); + + let prototypes = []; + let currentPrototype = Object.getPrototypeOf(this._actual); + while (currentPrototype) { + prototypes.push(currentPrototype.constructor.name); + currentPrototype = Object.getPrototypeOf(currentPrototype); + } + + return this._assert( + prototypes.includes(this._expected), + '${actual} inherits from "${expected}".', + '${actual} does not inherit from "${expected}".'); + } + } + + + // Task Class state enum. + const TaskState = {PENDING: 0, STARTED: 1, FINISHED: 2}; + + + /** + * @class Task + * @description WebAudio testing task. Managed by TaskRunner. + */ + class Task { + /** + * Task constructor. + * @param {Object} taskRunner Reference of associated task runner. + * @param {String||Object} taskLabel Task label if a string is given. This + * parameter can be a dictionary with the + * following fields. + * @param {String} taskLabel.label Task label. + * @param {String} taskLabel.description Description of task. + * @param {Function} taskFunction Task function to be performed. + * @return {Object} Task object. + */ + constructor(taskRunner, taskLabel, taskFunction) { + this._taskRunner = taskRunner; + this._taskFunction = taskFunction; + + if (typeof taskLabel === 'string') { + this._label = taskLabel; + this._description = null; + } else if (typeof taskLabel === 'object') { + if (typeof taskLabel.label !== 'string') { + _throwException('Task.constructor:: task label must be string.'); + } + this._label = taskLabel.label; + this._description = (typeof taskLabel.description === 'string') ? + taskLabel.description : + null; + } else { + _throwException( + 'Task.constructor:: task label must be a string or ' + + 'a dictionary.'); + } + + this._state = TaskState.PENDING; + this._result = true; + + this._totalAssertions = 0; + this._failedAssertions = 0; + } + + get label() { + return this._label; + } + + get state() { + return this._state; + } + + get result() { + return this._result; + } + + // Start the assertion chain. + should(actual, actualDescription) { + // If no argument is given, we cannot proceed. Halt. + if (arguments.length === 0) + _throwException('Task.should:: requires at least 1 argument.'); + + return new Should(this, actual, actualDescription); + } + + // Run this task. |this| task will be passed into the user-supplied test + // task function. + run(harnessTest) { + this._state = TaskState.STARTED; + this._harnessTest = harnessTest; + // Print out the task entry with label and description. + _logPassed( + '> [' + this._label + '] ' + + (this._description ? this._description : '')); + + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + let result = this._taskFunction(this, this.should.bind(this)); + if (result && typeof result.then === "function") { + result.then(() => this.done()).catch(reject); + } + }); + } + + // Update the task success based on the individual assertion/test inside. + update(subTask) { + // After one of tests fails within a task, the result is irreversible. + if (subTask.result === false) { + this._result = false; + this._failedAssertions++; + } + + this._totalAssertions++; + } + + // Finish the current task and start the next one if available. + done() { + assert_equals(this._state, TaskState.STARTED) + this._state = TaskState.FINISHED; + + let message = '< [' + this._label + '] '; + + if (this._result) { + message += 'All assertions passed. (total ' + this._totalAssertions + + ' assertions)'; + _logPassed(message); + } else { + message += this._failedAssertions + ' out of ' + this._totalAssertions + + ' assertions were failed.' + _logFailed(message); + } + + this._resolve(); + } + + // Runs |subTask| |time| milliseconds later. |setTimeout| is not allowed in + // WPT linter, so a thin wrapper around the harness's |step_timeout| is + // used here. Returns a Promise which is resolved after |subTask| runs. + timeout(subTask, time) { + return new Promise(resolve => { + this._harnessTest.step_timeout(() => { + let result = subTask(); + if (result && typeof result.then === "function") { + // Chain rejection directly to the harness test Promise, to report + // the rejection against the subtest even when the caller of + // timeout does not handle the rejection. + result.then(resolve, this._reject()); + } else { + resolve(); + } + }, time); + }); + } + + isPassed() { + return this._state === TaskState.FINISHED && this._result; + } + + toString() { + return '"' + this._label + '": ' + this._description; + } + } + + + /** + * @class TaskRunner + * @description WebAudio testing task runner. Manages tasks. + */ + class TaskRunner { + constructor() { + this._tasks = {}; + this._taskSequence = []; + + // Configure testharness.js for the async operation. + setup(new Function(), {explicit_done: true}); + } + + _finish() { + let numberOfFailures = 0; + for (let taskIndex in this._taskSequence) { + let task = this._tasks[this._taskSequence[taskIndex]]; + numberOfFailures += task.result ? 0 : 1; + } + + let prefix = '# AUDIT TASK RUNNER FINISHED: '; + if (numberOfFailures > 0) { + _logFailed( + prefix + numberOfFailures + ' out of ' + this._taskSequence.length + + ' tasks were failed.'); + } else { + _logPassed( + prefix + this._taskSequence.length + ' tasks ran successfully.'); + } + + return Promise.resolve(); + } + + // |taskLabel| can be either a string or a dictionary. See Task constructor + // for the detail. If |taskFunction| returns a thenable, then the task + // is considered complete when the thenable is fulfilled; otherwise the + // task must be completed with an explicit call to |task.done()|. + define(taskLabel, taskFunction) { + let task = new Task(this, taskLabel, taskFunction); + if (this._tasks.hasOwnProperty(task.label)) { + _throwException('Audit.define:: Duplicate task definition.'); + return; + } + this._tasks[task.label] = task; + this._taskSequence.push(task.label); + } + + // Start running all the tasks scheduled. Multiple task names can be passed + // to execute them sequentially. Zero argument will perform all defined + // tasks in the order of definition. + run() { + // Display the beginning of the test suite. + _logPassed('# AUDIT TASK RUNNER STARTED.'); + + // If the argument is specified, override the default task sequence with + // the specified one. + if (arguments.length > 0) { + this._taskSequence = []; + for (let i = 0; i < arguments.length; i++) { + let taskLabel = arguments[i]; + if (!this._tasks.hasOwnProperty(taskLabel)) { + _throwException('Audit.run:: undefined task.'); + } else if (this._taskSequence.includes(taskLabel)) { + _throwException('Audit.run:: duplicate task request.'); + } else { + this._taskSequence.push(taskLabel); + } + } + } + + if (this._taskSequence.length === 0) { + _throwException('Audit.run:: no task to run.'); + return; + } + + for (let taskIndex in this._taskSequence) { + let task = this._tasks[this._taskSequence[taskIndex]]; + // Some tests assume that tasks run in sequence, which is provided by + // promise_test(). + promise_test((t) => task.run(t), `Executing "${task.label}"`); + } + + // Schedule a summary report on completion. + promise_test(() => this._finish(), "Audit report"); + + // From testharness.js. The harness now need not wait for more subtests + // to be added. + _testharnessDone(); + } + } + + /** + * Load file from a given URL and pass ArrayBuffer to the following promise. + * @param {String} fileUrl file URL. + * @return {Promise} + * + * @example + * Audit.loadFileFromUrl('resources/my-sound.ogg').then((response) => { + * audioContext.decodeAudioData(response).then((audioBuffer) => { + * // Do something with AudioBuffer. + * }); + * }); + */ + function loadFileFromUrl(fileUrl) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open('GET', fileUrl, true); + xhr.responseType = 'arraybuffer'; + + xhr.onload = () => { + // |status = 0| is a workaround for the run_web_test.py server. We are + // speculating the server quits the transaction prematurely without + // completing the request. + if (xhr.status === 200 || xhr.status === 0) { + resolve(xhr.response); + } else { + let errorMessage = 'loadFile: Request failed when loading ' + + fileUrl + '. ' + xhr.statusText + '. (status = ' + xhr.status + + ')'; + if (reject) { + reject(errorMessage); + } else { + new Error(errorMessage); + } + } + }; + + xhr.onerror = (event) => { + let errorMessage = + 'loadFile: Network failure when loading ' + fileUrl + '.'; + if (reject) { + reject(errorMessage); + } else { + new Error(errorMessage); + } + }; + + xhr.send(); + }); + } + + /** + * @class Audit + * @description A WebAudio layout test task manager. + * @example + * let audit = Audit.createTaskRunner(); + * audit.define('first-task', function (task, should) { + * should(someValue).beEqualTo(someValue); + * task.done(); + * }); + * audit.run(); + */ + return { + + /** + * Creates an instance of Audit task runner. + * @param {Object} options Options for task runner. + * @param {Boolean} options.requireResultFile True if the test suite + * requires explicit text + * comparison with the expected + * result file. + */ + createTaskRunner: function(options) { + if (options && options.requireResultFile == true) { + _logError( + 'this test requires the explicit comparison with the ' + + 'expected result when it runs with run_web_tests.py.'); + } + + return new TaskRunner(); + }, + + /** + * Load file from a given URL and pass ArrayBuffer to the following promise. + * See |loadFileFromUrl| method for the detail. + */ + loadFileFromUrl: loadFileFromUrl + + }; + +})(); |