// 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 }; })();