| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // 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; |
| |
| // debugger; |
| |
| 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 |
| |
| }; |
| |
| })(); |