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