audit.js (46835B)
1 // Copyright 2016 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // See https://github.com/web-platform-tests/wpt/issues/12781 for information on 6 // the purpose of audit.js, and why testharness.js does not suffice. 7 8 /** 9 * @fileOverview WebAudio layout test utility library. Built around W3C's 10 * testharness.js. Includes asynchronous test task manager, 11 * assertion utilities. 12 * @dependency testharness.js 13 */ 14 15 16 (function() { 17 18 'use strict'; 19 20 // Selected methods from testharness.js. 21 let testharnessProperties = [ 22 'test', 'async_test', 'promise_test', 'promise_rejects_js', 'generate_tests', 23 'setup', 'done', 'assert_true', 'assert_false' 24 ]; 25 26 // Check if testharness.js is properly loaded. Throw otherwise. 27 for (let name in testharnessProperties) { 28 if (!self.hasOwnProperty(testharnessProperties[name])) 29 throw new Error('Cannot proceed. testharness.js is not loaded.'); 30 } 31 })(); 32 33 34 window.Audit = (function() { 35 36 'use strict'; 37 38 // NOTE: Moving this method (or any other code above) will change the location 39 // of 'CONSOLE ERROR...' message in the expected text files. 40 function _logError(message) { 41 console.error('[audit.js] ' + message); 42 } 43 44 function _logPassed(message) { 45 test(function(arg) { 46 assert_true(true); 47 }, message); 48 } 49 50 function _logFailed(message, detail) { 51 test(function() { 52 assert_true(false, detail); 53 }, message); 54 } 55 56 function _throwException(message) { 57 throw new Error(message); 58 } 59 60 // TODO(hongchan): remove this hack after confirming all the tests are 61 // finished correctly. (crbug.com/708817) 62 const _testharnessDone = window.done; 63 window.done = () => { 64 _throwException('Do NOT call done() method from the test code.'); 65 }; 66 67 // Generate a descriptive string from a target value in various types. 68 function _generateDescription(target, options) { 69 let targetString; 70 71 switch (typeof target) { 72 case 'object': 73 // Handle Arrays. 74 if (target instanceof Array || target instanceof Float32Array || 75 target instanceof Float64Array || target instanceof Uint8Array) { 76 let arrayElements = target.length < options.numberOfArrayElements ? 77 String(target) : 78 String(target.slice(0, options.numberOfArrayElements)) + '...'; 79 targetString = '[' + arrayElements + ']'; 80 } else if (target === null) { 81 targetString = String(target); 82 } else { 83 targetString = '' + String(target).split(/[\s\]]/)[1]; 84 } 85 break; 86 case 'function': 87 if (Error.isPrototypeOf(target)) { 88 targetString = "EcmaScript error " + target.name; 89 } else { 90 targetString = String(target); 91 } 92 break; 93 default: 94 targetString = String(target); 95 break; 96 } 97 98 return targetString; 99 } 100 101 // Return a string suitable for printing one failed element in 102 // |beCloseToArray|. 103 function _formatFailureEntry(index, actual, expected, abserr, threshold) { 104 return '\t[' + index + ']\t' + actual.toExponential(16) + '\t' + 105 expected.toExponential(16) + '\t' + abserr.toExponential(16) + '\t' + 106 (abserr / Math.abs(expected)).toExponential(16) + '\t' + 107 threshold.toExponential(16); 108 } 109 110 // Compute the error threshold criterion for |beCloseToArray| 111 function _closeToThreshold(abserr, relerr, expected) { 112 return Math.max(abserr, relerr * Math.abs(expected)); 113 } 114 115 /** 116 * @class Should 117 * @description Assertion subtask for the Audit task. 118 * @param {Task} parentTask Associated Task object. 119 * @param {Any} actual Target value to be tested. 120 * @param {String} actualDescription String description of the test target. 121 */ 122 class Should { 123 constructor(parentTask, actual, actualDescription) { 124 this._task = parentTask; 125 126 this._actual = actual; 127 this._actualDescription = (actualDescription || null); 128 this._expected = null; 129 this._expectedDescription = null; 130 131 this._detail = ''; 132 // If true and the test failed, print the actual value at the 133 // end of the message. 134 this._printActualForFailure = true; 135 136 this._result = null; 137 138 /** 139 * @param {Number} numberOfErrors Number of errors to be printed. 140 * @param {Number} numberOfArrayElements Number of array elements to be 141 * printed in the test log. 142 * @param {Boolean} verbose Verbose output from the assertion. 143 */ 144 this._options = { 145 numberOfErrors: 4, 146 numberOfArrayElements: 16, 147 verbose: false 148 }; 149 } 150 151 _processArguments(args) { 152 if (args.length === 0) 153 return; 154 155 if (args.length > 0) 156 this._expected = args[0]; 157 158 if (typeof args[1] === 'string') { 159 // case 1: (expected, description, options) 160 this._expectedDescription = args[1]; 161 Object.assign(this._options, args[2]); 162 } else if (typeof args[1] === 'object') { 163 // case 2: (expected, options) 164 Object.assign(this._options, args[1]); 165 } 166 } 167 168 _buildResultText() { 169 if (this._result === null) 170 _throwException('Illegal invocation: the assertion is not finished.'); 171 172 let actualString = _generateDescription(this._actual, this._options); 173 174 // Use generated text when the description is not provided. 175 if (!this._actualDescription) 176 this._actualDescription = actualString; 177 178 if (!this._expectedDescription) { 179 this._expectedDescription = 180 _generateDescription(this._expected, this._options); 181 } 182 183 // For the assertion with a single operand. 184 this._detail = 185 this._detail.replace(/\$\{actual\}/g, this._actualDescription); 186 187 // If there is a second operand (i.e. expected value), we have to build 188 // the string for it as well. 189 this._detail = 190 this._detail.replace(/\$\{expected\}/g, this._expectedDescription); 191 192 // If there is any property in |_options|, replace the property name 193 // with the value. 194 for (let name in this._options) { 195 if (name === 'numberOfErrors' || name === 'numberOfArrayElements' || 196 name === 'verbose') { 197 continue; 198 } 199 200 // The RegExp key string contains special character. Take care of it. 201 let re = '\$\{' + name + '\}'; 202 re = re.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); 203 this._detail = this._detail.replace( 204 new RegExp(re, 'g'), _generateDescription(this._options[name])); 205 } 206 207 // If the test failed, add the actual value at the end. 208 if (this._result === false && this._printActualForFailure === true) { 209 this._detail += ' Got ' + actualString + '.'; 210 } 211 } 212 213 _finalize() { 214 if (this._result) { 215 _logPassed(' ' + this._detail); 216 } else { 217 _logFailed('X ' + this._detail); 218 } 219 220 // This assertion is finished, so update the parent task accordingly. 221 this._task.update(this); 222 223 // TODO(hongchan): configurable 'detail' message. 224 } 225 226 _assert(condition, passDetail, failDetail) { 227 this._result = Boolean(condition); 228 this._detail = this._result ? passDetail : failDetail; 229 this._buildResultText(); 230 this._finalize(); 231 232 return this._result; 233 } 234 235 get result() { 236 return this._result; 237 } 238 239 get detail() { 240 return this._detail; 241 } 242 243 /** 244 * should() assertions. 245 * 246 * @example All the assertions can have 1, 2 or 3 arguments: 247 * should().doAssert(expected); 248 * should().doAssert(expected, options); 249 * should().doAssert(expected, expectedDescription, options); 250 * 251 * @param {Any} expected Expected value of the assertion. 252 * @param {String} expectedDescription Description of expected value. 253 * @param {Object} options Options for assertion. 254 * @param {Number} options.numberOfErrors Number of errors to be printed. 255 * (if applicable) 256 * @param {Number} options.numberOfArrayElements Number of array elements 257 * to be printed. (if 258 * applicable) 259 * @notes Some assertions can have additional options for their specific 260 * testing. 261 */ 262 263 /** 264 * Check if |actual| exists. 265 * 266 * @example 267 * should({}, 'An empty object').exist(); 268 * @result 269 * "PASS An empty object does exist." 270 */ 271 exist() { 272 return this._assert( 273 this._actual !== null && this._actual !== undefined, 274 '${actual} does exist.', '${actual} does not exist.'); 275 } 276 277 /** 278 * Check if |actual| operation wrapped in a function throws an exception 279 * with a expected error type correctly. |expected| is optional. If it is an 280 * instance of DOMException, then the description (second argument) can be 281 * provided to be more strict about the expected exception type. |expected| 282 * also can be other generic error types such as TypeError, RangeError or 283 * etc. 284 * 285 * @example 286 * should(() => { let a = b; }, 'A bad code').throw(); 287 * should(() => { new SomeConstructor(); }, 'A bad construction') 288 * .throw(DOMException, 'NotSupportedError'); 289 * should(() => { let c = d; }, 'Assigning d to c') 290 * .throw(ReferenceError); 291 * should(() => { let e = f; }, 'Assigning e to f') 292 * .throw(ReferenceError, { omitErrorMessage: true }); 293 * 294 * @result 295 * "PASS A bad code threw an exception of ReferenceError: b is not 296 * defined." 297 * "PASS A bad construction threw DOMException:NotSupportedError." 298 * "PASS Assigning d to c threw ReferenceError: d is not defined." 299 * "PASS Assigning e to f threw ReferenceError: [error message 300 * omitted]." 301 */ 302 throw() { 303 this._processArguments(arguments); 304 this._printActualForFailure = false; 305 306 let didThrowCorrectly = false; 307 let passDetail, failDetail; 308 309 try { 310 // This should throw. 311 this._actual(); 312 // Catch did not happen, so the test is failed. 313 failDetail = '${actual} did not throw an exception.'; 314 } catch (error) { 315 let errorMessage = this._options.omitErrorMessage ? 316 ': [error message omitted]' : 317 ': "' + error.message + '"'; 318 if (this._expected === null || this._expected === undefined) { 319 // The expected error type was not given. 320 didThrowCorrectly = true; 321 passDetail = '${actual} threw ' + error.name + errorMessage + '.'; 322 } else if (this._expected === DOMException && 323 this._expectedDescription !== undefined) { 324 // Handles DOMException with an expected exception name. 325 if (this._expectedDescription === error.name) { 326 didThrowCorrectly = true; 327 passDetail = '${actual} threw ${expected}' + errorMessage + '.'; 328 } else { 329 didThrowCorrectly = false; 330 failDetail = 331 '${actual} threw "' + error.name + '" instead of ${expected}.'; 332 } 333 } else if (this._expected == error.constructor) { 334 // Handler other error types. 335 didThrowCorrectly = true; 336 passDetail = '${actual} threw ' + error.name + errorMessage + '.'; 337 } else { 338 didThrowCorrectly = false; 339 failDetail = 340 '${actual} threw "' + error.name + '" instead of ${expected}.'; 341 } 342 } 343 344 return this._assert(didThrowCorrectly, passDetail, failDetail); 345 } 346 347 /** 348 * Check if |actual| operation wrapped in a function does not throws an 349 * exception correctly. 350 * 351 * @example 352 * should(() => { let foo = 'bar'; }, 'let foo = "bar"').notThrow(); 353 * 354 * @result 355 * "PASS let foo = "bar" did not throw an exception." 356 */ 357 notThrow() { 358 this._printActualForFailure = false; 359 360 let didThrowCorrectly = false; 361 let passDetail, failDetail; 362 363 try { 364 this._actual(); 365 passDetail = '${actual} did not throw an exception.'; 366 } catch (error) { 367 didThrowCorrectly = true; 368 failDetail = '${actual} incorrectly threw ' + error.name + ': "' + 369 error.message + '".'; 370 } 371 372 return this._assert(!didThrowCorrectly, passDetail, failDetail); 373 } 374 375 /** 376 * Check if |actual| promise is resolved correctly. Note that the returned 377 * result from promise object will be passed to the following then() 378 * function. 379 * 380 * @example 381 * should('My promise', promise).beResolve().then((result) => { 382 * log(result); 383 * }); 384 * 385 * @result 386 * "PASS My promise resolved correctly." 387 * "FAIL X My promise rejected *INCORRECTLY* with _ERROR_." 388 */ 389 beResolved() { 390 return this._actual.then( 391 function(result) { 392 this._assert(true, '${actual} resolved correctly.', null); 393 return result; 394 }.bind(this), 395 function(error) { 396 this._assert( 397 false, null, 398 '${actual} rejected incorrectly with ' + error + '.'); 399 }.bind(this)); 400 } 401 402 /** 403 * Check if |actual| promise is rejected correctly. 404 * 405 * @example 406 * should('My promise', promise).beRejected().then(nextStuff); 407 * 408 * @result 409 * "PASS My promise rejected correctly (with _ERROR_)." 410 * "FAIL X My promise resolved *INCORRECTLY*." 411 */ 412 beRejected() { 413 return this._actual.then( 414 function() { 415 this._assert(false, null, '${actual} resolved incorrectly.'); 416 }.bind(this), 417 function(error) { 418 this._assert( 419 true, '${actual} rejected correctly with ' + error + '.', null); 420 }.bind(this)); 421 } 422 423 /** 424 * Check if |actual| promise is rejected correctly. 425 * 426 * @example 427 * should(promise, 'My promise').beRejectedWith('_ERROR_').then(); 428 * 429 * @result 430 * "PASS My promise rejected correctly with _ERROR_." 431 * "FAIL X My promise rejected correctly but got _ACTUAL_ERROR instead of 432 * _EXPECTED_ERROR_." 433 * "FAIL X My promise resolved incorrectly." 434 */ 435 beRejectedWith() { 436 this._processArguments(arguments); 437 438 return this._actual.then( 439 function() { 440 this._assert(false, null, '${actual} resolved incorrectly.'); 441 }.bind(this), 442 function(error) { 443 if (this._expected !== error.name) { 444 this._assert( 445 false, null, 446 '${actual} rejected correctly but got ' + error.name + 447 ' instead of ' + this._expected + '.'); 448 } else { 449 this._assert( 450 true, 451 '${actual} rejected correctly with ' + this._expected + '.', 452 null); 453 } 454 }.bind(this)); 455 } 456 457 /** 458 * Check if |actual| is a boolean true. 459 * 460 * @example 461 * should(3 < 5, '3 < 5').beTrue(); 462 * 463 * @result 464 * "PASS 3 < 5 is true." 465 */ 466 beTrue() { 467 return this._assert( 468 this._actual === true, '${actual} is true.', 469 '${actual} is not true.'); 470 } 471 472 /** 473 * Check if |actual| is a boolean false. 474 * 475 * @example 476 * should(3 > 5, '3 > 5').beFalse(); 477 * 478 * @result 479 * "PASS 3 > 5 is false." 480 */ 481 beFalse() { 482 return this._assert( 483 this._actual === false, '${actual} is false.', 484 '${actual} is not false.'); 485 } 486 487 /** 488 * Check if |actual| is strictly equal to |expected|. (no type coercion) 489 * 490 * @example 491 * should(1).beEqualTo(1); 492 * 493 * @result 494 * "PASS 1 is equal to 1." 495 */ 496 beEqualTo() { 497 this._processArguments(arguments); 498 return this._assert( 499 this._actual === this._expected, '${actual} is equal to ${expected}.', 500 '${actual} is not equal to ${expected}.'); 501 } 502 503 /** 504 * Check if |actual| is not equal to |expected|. 505 * 506 * @example 507 * should(1).notBeEqualTo(2); 508 * 509 * @result 510 * "PASS 1 is not equal to 2." 511 */ 512 notBeEqualTo() { 513 this._processArguments(arguments); 514 return this._assert( 515 this._actual !== this._expected, 516 '${actual} is not equal to ${expected}.', 517 '${actual} should not be equal to ${expected}.'); 518 } 519 520 /** 521 * check if |actual| is NaN 522 * 523 * @example 524 * should(NaN).beNaN(); 525 * 526 * @result 527 * "PASS NaN is NaN" 528 * 529 */ 530 beNaN() { 531 this._processArguments(arguments); 532 return this._assert( 533 isNaN(this._actual), 534 '${actual} is NaN.', 535 '${actual} is not NaN but should be.'); 536 } 537 538 /** 539 * check if |actual| is NOT NaN 540 * 541 * @example 542 * should(42).notBeNaN(); 543 * 544 * @result 545 * "PASS 42 is not NaN" 546 * 547 */ 548 notBeNaN() { 549 this._processArguments(arguments); 550 return this._assert( 551 !isNaN(this._actual), 552 '${actual} is not NaN.', 553 '${actual} is NaN but should not be.'); 554 } 555 556 /** 557 * Check if |actual| is greater than |expected|. 558 * 559 * @example 560 * should(2).beGreaterThanOrEqualTo(2); 561 * 562 * @result 563 * "PASS 2 is greater than or equal to 2." 564 */ 565 beGreaterThan() { 566 this._processArguments(arguments); 567 return this._assert( 568 this._actual > this._expected, 569 '${actual} is greater than ${expected}.', 570 '${actual} is not greater than ${expected}.'); 571 } 572 573 /** 574 * Check if |actual| is greater than or equal to |expected|. 575 * 576 * @example 577 * should(2).beGreaterThan(1); 578 * 579 * @result 580 * "PASS 2 is greater than 1." 581 */ 582 beGreaterThanOrEqualTo() { 583 this._processArguments(arguments); 584 return this._assert( 585 this._actual >= this._expected, 586 '${actual} is greater than or equal to ${expected}.', 587 '${actual} is not greater than or equal to ${expected}.'); 588 } 589 590 /** 591 * Check if |actual| is less than |expected|. 592 * 593 * @example 594 * should(1).beLessThan(2); 595 * 596 * @result 597 * "PASS 1 is less than 2." 598 */ 599 beLessThan() { 600 this._processArguments(arguments); 601 return this._assert( 602 this._actual < this._expected, '${actual} is less than ${expected}.', 603 '${actual} is not less than ${expected}.'); 604 } 605 606 /** 607 * Check if |actual| is less than or equal to |expected|. 608 * 609 * @example 610 * should(1).beLessThanOrEqualTo(1); 611 * 612 * @result 613 * "PASS 1 is less than or equal to 1." 614 */ 615 beLessThanOrEqualTo() { 616 this._processArguments(arguments); 617 return this._assert( 618 this._actual <= this._expected, 619 '${actual} is less than or equal to ${expected}.', 620 '${actual} is not less than or equal to ${expected}.'); 621 } 622 623 /** 624 * Check if |actual| array is filled with a constant |expected| value. 625 * 626 * @example 627 * should([1, 1, 1]).beConstantValueOf(1); 628 * 629 * @result 630 * "PASS [1,1,1] contains only the constant 1." 631 */ 632 beConstantValueOf() { 633 this._processArguments(arguments); 634 this._printActualForFailure = false; 635 636 let passed = true; 637 let passDetail, failDetail; 638 let errors = {}; 639 640 let actual = this._actual; 641 let expected = this._expected; 642 for (let index = 0; index < actual.length; ++index) { 643 if (actual[index] !== expected) 644 errors[index] = actual[index]; 645 } 646 647 let numberOfErrors = Object.keys(errors).length; 648 passed = numberOfErrors === 0; 649 650 if (passed) { 651 passDetail = '${actual} contains only the constant ${expected}.'; 652 } else { 653 let counter = 0; 654 failDetail = 655 '${actual}: Expected ${expected} for all values but found ' + 656 numberOfErrors + ' unexpected values: '; 657 failDetail += '\n\tIndex\tActual'; 658 for (let errorIndex in errors) { 659 failDetail += '\n\t[' + errorIndex + ']' + 660 '\t' + errors[errorIndex]; 661 if (++counter >= this._options.numberOfErrors) { 662 failDetail += 663 '\n\t...and ' + (numberOfErrors - counter) + ' more errors.'; 664 break; 665 } 666 } 667 } 668 669 return this._assert(passed, passDetail, failDetail); 670 } 671 672 /** 673 * Check if |actual| array is not filled with a constant |expected| value. 674 * 675 * @example 676 * should([1, 0, 1]).notBeConstantValueOf(1); 677 * should([0, 0, 0]).notBeConstantValueOf(0); 678 * 679 * @result 680 * "PASS [1,0,1] is not constantly 1 (contains 1 different value)." 681 * "FAIL X [0,0,0] should have contain at least one value different 682 * from 0." 683 */ 684 notBeConstantValueOf() { 685 this._processArguments(arguments); 686 this._printActualForFailure = false; 687 688 let passed = true; 689 let passDetail; 690 let failDetail; 691 let differences = {}; 692 693 let actual = this._actual; 694 let expected = this._expected; 695 for (let index = 0; index < actual.length; ++index) { 696 if (actual[index] !== expected) 697 differences[index] = actual[index]; 698 } 699 700 let numberOfDifferences = Object.keys(differences).length; 701 passed = numberOfDifferences > 0; 702 703 if (passed) { 704 let valueString = numberOfDifferences > 1 ? 'values' : 'value'; 705 passDetail = '${actual} is not constantly ${expected} (contains ' + 706 numberOfDifferences + ' different ' + valueString + ').'; 707 } else { 708 failDetail = '${actual} should have contain at least one value ' + 709 'different from ${expected}.'; 710 } 711 712 return this._assert(passed, passDetail, failDetail); 713 } 714 715 /** 716 * Check if |actual| array is identical to |expected| array element-wise. 717 * 718 * @example 719 * should([1, 2, 3]).beEqualToArray([1, 2, 3]); 720 * 721 * @result 722 * "[1,2,3] is identical to the array [1,2,3]." 723 */ 724 beEqualToArray() { 725 this._processArguments(arguments); 726 this._printActualForFailure = false; 727 728 let passed = true; 729 let passDetail, failDetail; 730 let errorIndices = []; 731 732 if (this._actual.length !== this._expected.length) { 733 passed = false; 734 failDetail = 'The array length does not match.'; 735 return this._assert(passed, passDetail, failDetail); 736 } 737 738 let actual = this._actual; 739 let expected = this._expected; 740 for (let index = 0; index < actual.length; ++index) { 741 if (actual[index] !== expected[index]) 742 errorIndices.push(index); 743 } 744 745 passed = errorIndices.length === 0; 746 747 if (passed) { 748 passDetail = '${actual} is identical to the array ${expected}.'; 749 } else { 750 let counter = 0; 751 failDetail = 752 '${actual} expected to be equal to the array ${expected} ' + 753 'but differs in ' + errorIndices.length + ' places:' + 754 '\n\tIndex\tActual\t\t\tExpected'; 755 for (let index of errorIndices) { 756 failDetail += '\n\t[' + index + ']' + 757 '\t' + this._actual[index].toExponential(16) + '\t' + 758 this._expected[index].toExponential(16); 759 if (++counter >= this._options.numberOfErrors) { 760 failDetail += '\n\t...and ' + (errorIndices.length - counter) + 761 ' more errors.'; 762 break; 763 } 764 } 765 } 766 767 return this._assert(passed, passDetail, failDetail); 768 } 769 770 /** 771 * Check if |actual| array contains only the values in |expected| in the 772 * order of values in |expected|. 773 * 774 * @example 775 * Should([1, 1, 3, 3, 2], 'My random array').containValues([1, 3, 2]); 776 * 777 * @result 778 * "PASS [1,1,3,3,2] contains all the expected values in the correct 779 * order: [1,3,2]. 780 */ 781 containValues() { 782 this._processArguments(arguments); 783 this._printActualForFailure = false; 784 785 let passed = true; 786 let indexedActual = []; 787 let firstErrorIndex = null; 788 789 // Collect the unique value sequence from the actual. 790 for (let i = 0, prev = null; i < this._actual.length; i++) { 791 if (this._actual[i] !== prev) { 792 indexedActual.push({index: i, value: this._actual[i]}); 793 prev = this._actual[i]; 794 } 795 } 796 797 // Compare against the expected sequence. 798 let failMessage = 799 '${actual} expected to have the value sequence of ${expected} but ' + 800 'got '; 801 if (this._expected.length === indexedActual.length) { 802 for (let j = 0; j < this._expected.length; j++) { 803 if (this._expected[j] !== indexedActual[j].value) { 804 firstErrorIndex = indexedActual[j].index; 805 passed = false; 806 failMessage += this._actual[firstErrorIndex] + ' at index ' + 807 firstErrorIndex + '.'; 808 break; 809 } 810 } 811 } else { 812 passed = false; 813 let indexedValues = indexedActual.map(x => x.value); 814 failMessage += `${indexedActual.length} values, [${ 815 indexedValues}], instead of ${this._expected.length}.`; 816 } 817 818 return this._assert( 819 passed, 820 '${actual} contains all the expected values in the correct order: ' + 821 '${expected}.', 822 failMessage); 823 } 824 825 /** 826 * Check if |actual| array does not have any glitches. Note that |threshold| 827 * is not optional and is to define the desired threshold value. 828 * 829 * @example 830 * should([0.5, 0.5, 0.55, 0.5, 0.45, 0.5]).notGlitch(0.06); 831 * 832 * @result 833 * "PASS [0.5,0.5,0.55,0.5,0.45,0.5] has no glitch above the threshold 834 * of 0.06." 835 * 836 */ 837 notGlitch() { 838 this._processArguments(arguments); 839 this._printActualForFailure = false; 840 841 let passed = true; 842 let passDetail, failDetail; 843 844 let actual = this._actual; 845 let expected = this._expected; 846 for (let index = 0; index < actual.length; ++index) { 847 let diff = Math.abs(actual[index - 1] - actual[index]); 848 if (diff >= expected) { 849 passed = false; 850 failDetail = '${actual} has a glitch at index ' + index + 851 ' of size ' + diff + '.'; 852 } 853 } 854 855 passDetail = 856 '${actual} has no glitch above the threshold of ${expected}.'; 857 858 return this._assert(passed, passDetail, failDetail); 859 } 860 861 /** 862 * Check if |actual| is close to |expected| using the given relative error 863 * |threshold|. 864 * 865 * @example 866 * should(2.3).beCloseTo(2, { threshold: 0.3 }); 867 * 868 * @result 869 * "PASS 2.3 is 2 within an error of 0.3." 870 * @param {Object} options Options for assertion. 871 * @param {Number} options.threshold Threshold value for the comparison. 872 */ 873 beCloseTo() { 874 this._processArguments(arguments); 875 876 // The threshold is relative except when |expected| is zero, in which case 877 // it is absolute. 878 let absExpected = this._expected ? Math.abs(this._expected) : 1; 879 let error = Math.abs(this._actual - this._expected) / absExpected; 880 881 return this._assert( 882 error <= this._options.threshold, 883 '${actual} is ${expected} within an error of ${threshold}.', 884 '${actual} is not close to ${expected} within a relative error of ' + 885 '${threshold} (RelErr=' + error + ').'); 886 } 887 888 /** 889 * Check if |target| array is close to |expected| array element-wise within 890 * a certain error bound given by the |options|. 891 * 892 * The error criterion is: 893 * abs(actual[k] - expected[k]) < max(absErr, relErr * abs(expected)) 894 * 895 * If nothing is given for |options|, then absErr = relErr = 0. If 896 * absErr = 0, then the error criterion is a relative error. A non-zero 897 * absErr value produces a mix intended to handle the case where the 898 * expected value is 0, allowing the target value to differ by absErr from 899 * the expected. 900 * 901 * @param {Number} options.absoluteThreshold Absolute threshold. 902 * @param {Number} options.relativeThreshold Relative threshold. 903 */ 904 beCloseToArray() { 905 this._processArguments(arguments); 906 this._printActualForFailure = false; 907 908 let passed = true; 909 let passDetail, failDetail; 910 911 // Parsing options. 912 let absErrorThreshold = (this._options.absoluteThreshold || 0); 913 let relErrorThreshold = (this._options.relativeThreshold || 0); 914 915 // A collection of all of the values that satisfy the error criterion. 916 // This holds the absolute difference between the target element and the 917 // expected element. 918 let errors = {}; 919 920 // Keep track of the max absolute error found. 921 let maxAbsError = -Infinity, maxAbsErrorIndex = -1; 922 923 // Keep track of the max relative error found, ignoring cases where the 924 // relative error is Infinity because the expected value is 0. 925 let maxRelError = -Infinity, maxRelErrorIndex = -1; 926 927 let actual = this._actual; 928 let expected = this._expected; 929 930 for (let index = 0; index < expected.length; ++index) { 931 let diff = Math.abs(actual[index] - expected[index]); 932 let absExpected = Math.abs(expected[index]); 933 let relError = diff / absExpected; 934 935 if (diff > 936 Math.max(absErrorThreshold, relErrorThreshold * absExpected)) { 937 if (diff > maxAbsError) { 938 maxAbsErrorIndex = index; 939 maxAbsError = diff; 940 } 941 942 if (!isNaN(relError) && relError > maxRelError) { 943 maxRelErrorIndex = index; 944 maxRelError = relError; 945 } 946 947 errors[index] = diff; 948 } 949 } 950 951 let numberOfErrors = Object.keys(errors).length; 952 let maxAllowedErrorDetail = JSON.stringify({ 953 absoluteThreshold: absErrorThreshold, 954 relativeThreshold: relErrorThreshold 955 }); 956 957 if (numberOfErrors === 0) { 958 // The assertion was successful. 959 passDetail = '${actual} equals ${expected} with an element-wise ' + 960 'tolerance of ' + maxAllowedErrorDetail + '.'; 961 } else { 962 // Failed. Prepare the detailed failure log. 963 passed = false; 964 failDetail = '${actual} does not equal ${expected} with an ' + 965 'element-wise tolerance of ' + maxAllowedErrorDetail + '.\n'; 966 967 // Print out actual, expected, absolute error, and relative error. 968 let counter = 0; 969 failDetail += '\tIndex\tActual\t\t\tExpected\t\tAbsError' + 970 '\t\tRelError\t\tTest threshold'; 971 let printedIndices = []; 972 for (let index in errors) { 973 failDetail += 974 '\n' + 975 _formatFailureEntry( 976 index, actual[index], expected[index], errors[index], 977 _closeToThreshold( 978 absErrorThreshold, relErrorThreshold, expected[index])); 979 980 printedIndices.push(index); 981 if (++counter > this._options.numberOfErrors) { 982 failDetail += 983 '\n\t...and ' + (numberOfErrors - counter) + ' more errors.'; 984 break; 985 } 986 } 987 988 // Finalize the error log: print out the location of both the maxAbs 989 // error and the maxRel error so we can adjust thresholds appropriately 990 // in the test. 991 failDetail += '\n' + 992 '\tMax AbsError of ' + maxAbsError.toExponential(16) + 993 ' at index of ' + maxAbsErrorIndex + '.\n'; 994 if (printedIndices.find(element => { 995 return element == maxAbsErrorIndex; 996 }) === undefined) { 997 // Print an entry for this index if we haven't already. 998 failDetail += 999 _formatFailureEntry( 1000 maxAbsErrorIndex, actual[maxAbsErrorIndex], 1001 expected[maxAbsErrorIndex], errors[maxAbsErrorIndex], 1002 _closeToThreshold( 1003 absErrorThreshold, relErrorThreshold, 1004 expected[maxAbsErrorIndex])) + 1005 '\n'; 1006 } 1007 failDetail += '\tMax RelError of ' + maxRelError.toExponential(16) + 1008 ' at index of ' + maxRelErrorIndex + '.\n'; 1009 if (printedIndices.find(element => { 1010 return element == maxRelErrorIndex; 1011 }) === undefined) { 1012 // Print an entry for this index if we haven't already. 1013 failDetail += 1014 _formatFailureEntry( 1015 maxRelErrorIndex, actual[maxRelErrorIndex], 1016 expected[maxRelErrorIndex], errors[maxRelErrorIndex], 1017 _closeToThreshold( 1018 absErrorThreshold, relErrorThreshold, 1019 expected[maxRelErrorIndex])) + 1020 '\n'; 1021 } 1022 } 1023 1024 return this._assert(passed, passDetail, failDetail); 1025 } 1026 1027 /** 1028 * A temporary escape hat for printing an in-task message. The description 1029 * for the |actual| is required to get the message printed properly. 1030 * 1031 * TODO(hongchan): remove this method when the transition from the old Audit 1032 * to the new Audit is completed. 1033 * @example 1034 * should(true, 'The message is').message('truthful!', 'false!'); 1035 * 1036 * @result 1037 * "PASS The message is truthful!" 1038 */ 1039 message(passDetail, failDetail) { 1040 return this._assert( 1041 this._actual, '${actual} ' + passDetail, '${actual} ' + failDetail); 1042 } 1043 1044 /** 1045 * Check if |expected| property is truly owned by |actual| object. 1046 * 1047 * @example 1048 * should(BaseAudioContext.prototype, 1049 * 'BaseAudioContext.prototype').haveOwnProperty('createGain'); 1050 * 1051 * @result 1052 * "PASS BaseAudioContext.prototype has an own property of 1053 * 'createGain'." 1054 */ 1055 haveOwnProperty() { 1056 this._processArguments(arguments); 1057 1058 return this._assert( 1059 this._actual.hasOwnProperty(this._expected), 1060 '${actual} has an own property of "${expected}".', 1061 '${actual} does not own the property of "${expected}".'); 1062 } 1063 1064 1065 /** 1066 * Check if |expected| property is not owned by |actual| object. 1067 * 1068 * @example 1069 * should(BaseAudioContext.prototype, 1070 * 'BaseAudioContext.prototype') 1071 * .notHaveOwnProperty('startRendering'); 1072 * 1073 * @result 1074 * "PASS BaseAudioContext.prototype does not have an own property of 1075 * 'startRendering'." 1076 */ 1077 notHaveOwnProperty() { 1078 this._processArguments(arguments); 1079 1080 return this._assert( 1081 !this._actual.hasOwnProperty(this._expected), 1082 '${actual} does not have an own property of "${expected}".', 1083 '${actual} has an own the property of "${expected}".') 1084 } 1085 1086 1087 /** 1088 * Check if an object is inherited from a class. This looks up the entire 1089 * prototype chain of a given object and tries to find a match. 1090 * 1091 * @example 1092 * should(sourceNode, 'A buffer source node') 1093 * .inheritFrom('AudioScheduledSourceNode'); 1094 * 1095 * @result 1096 * "PASS A buffer source node inherits from 'AudioScheduledSourceNode'." 1097 */ 1098 inheritFrom() { 1099 this._processArguments(arguments); 1100 1101 let prototypes = []; 1102 let currentPrototype = Object.getPrototypeOf(this._actual); 1103 while (currentPrototype) { 1104 prototypes.push(currentPrototype.constructor.name); 1105 currentPrototype = Object.getPrototypeOf(currentPrototype); 1106 } 1107 1108 return this._assert( 1109 prototypes.includes(this._expected), 1110 '${actual} inherits from "${expected}".', 1111 '${actual} does not inherit from "${expected}".'); 1112 } 1113 } 1114 1115 1116 // Task Class state enum. 1117 const TaskState = {PENDING: 0, STARTED: 1, FINISHED: 2}; 1118 1119 1120 /** 1121 * @class Task 1122 * @description WebAudio testing task. Managed by TaskRunner. 1123 */ 1124 class Task { 1125 /** 1126 * Task constructor. 1127 * @param {Object} taskRunner Reference of associated task runner. 1128 * @param {String||Object} taskLabel Task label if a string is given. This 1129 * parameter can be a dictionary with the 1130 * following fields. 1131 * @param {String} taskLabel.label Task label. 1132 * @param {String} taskLabel.description Description of task. 1133 * @param {Function} taskFunction Task function to be performed. 1134 * @return {Object} Task object. 1135 */ 1136 constructor(taskRunner, taskLabel, taskFunction) { 1137 this._taskRunner = taskRunner; 1138 this._taskFunction = taskFunction; 1139 1140 if (typeof taskLabel === 'string') { 1141 this._label = taskLabel; 1142 this._description = null; 1143 } else if (typeof taskLabel === 'object') { 1144 if (typeof taskLabel.label !== 'string') { 1145 _throwException('Task.constructor:: task label must be string.'); 1146 } 1147 this._label = taskLabel.label; 1148 this._description = (typeof taskLabel.description === 'string') ? 1149 taskLabel.description : 1150 null; 1151 } else { 1152 _throwException( 1153 'Task.constructor:: task label must be a string or ' + 1154 'a dictionary.'); 1155 } 1156 1157 this._state = TaskState.PENDING; 1158 this._result = true; 1159 1160 this._totalAssertions = 0; 1161 this._failedAssertions = 0; 1162 } 1163 1164 get label() { 1165 return this._label; 1166 } 1167 1168 get state() { 1169 return this._state; 1170 } 1171 1172 get result() { 1173 return this._result; 1174 } 1175 1176 // Start the assertion chain. 1177 should(actual, actualDescription) { 1178 // If no argument is given, we cannot proceed. Halt. 1179 if (arguments.length === 0) 1180 _throwException('Task.should:: requires at least 1 argument.'); 1181 1182 return new Should(this, actual, actualDescription); 1183 } 1184 1185 // Run this task. |this| task will be passed into the user-supplied test 1186 // task function. 1187 run(harnessTest) { 1188 this._state = TaskState.STARTED; 1189 this._harnessTest = harnessTest; 1190 // Print out the task entry with label and description. 1191 _logPassed( 1192 '> [' + this._label + '] ' + 1193 (this._description ? this._description : '')); 1194 1195 return new Promise((resolve, reject) => { 1196 this._resolve = resolve; 1197 this._reject = reject; 1198 let result = this._taskFunction(this, this.should.bind(this)); 1199 if (result && typeof result.then === "function") { 1200 result.then(() => this.done()).catch(reject); 1201 } 1202 }); 1203 } 1204 1205 // Update the task success based on the individual assertion/test inside. 1206 update(subTask) { 1207 // After one of tests fails within a task, the result is irreversible. 1208 if (subTask.result === false) { 1209 this._result = false; 1210 this._failedAssertions++; 1211 } 1212 1213 this._totalAssertions++; 1214 } 1215 1216 // Finish the current task and start the next one if available. 1217 done() { 1218 assert_equals(this._state, TaskState.STARTED) 1219 this._state = TaskState.FINISHED; 1220 1221 let message = '< [' + this._label + '] '; 1222 1223 if (this._result) { 1224 message += 'All assertions passed. (total ' + this._totalAssertions + 1225 ' assertions)'; 1226 _logPassed(message); 1227 } else { 1228 message += this._failedAssertions + ' out of ' + this._totalAssertions + 1229 ' assertions were failed.' 1230 _logFailed(message); 1231 } 1232 1233 this._resolve(); 1234 } 1235 1236 // Runs |subTask| |time| milliseconds later. |setTimeout| is not allowed in 1237 // WPT linter, so a thin wrapper around the harness's |step_timeout| is 1238 // used here. Returns a Promise which is resolved after |subTask| runs. 1239 timeout(subTask, time) { 1240 return new Promise(resolve => { 1241 this._harnessTest.step_timeout(() => { 1242 let result = subTask(); 1243 if (result && typeof result.then === "function") { 1244 // Chain rejection directly to the harness test Promise, to report 1245 // the rejection against the subtest even when the caller of 1246 // timeout does not handle the rejection. 1247 result.then(resolve, this._reject()); 1248 } else { 1249 resolve(); 1250 } 1251 }, time); 1252 }); 1253 } 1254 1255 isPassed() { 1256 return this._state === TaskState.FINISHED && this._result; 1257 } 1258 1259 toString() { 1260 return '"' + this._label + '": ' + this._description; 1261 } 1262 } 1263 1264 1265 /** 1266 * @class TaskRunner 1267 * @description WebAudio testing task runner. Manages tasks. 1268 */ 1269 class TaskRunner { 1270 constructor() { 1271 this._tasks = {}; 1272 this._taskSequence = []; 1273 1274 // Configure testharness.js for the async operation. 1275 setup(new Function(), {explicit_done: true}); 1276 } 1277 1278 _finish() { 1279 let numberOfFailures = 0; 1280 for (let taskIndex in this._taskSequence) { 1281 let task = this._tasks[this._taskSequence[taskIndex]]; 1282 numberOfFailures += task.result ? 0 : 1; 1283 } 1284 1285 let prefix = '# AUDIT TASK RUNNER FINISHED: '; 1286 if (numberOfFailures > 0) { 1287 _logFailed( 1288 prefix + numberOfFailures + ' out of ' + this._taskSequence.length + 1289 ' tasks were failed.'); 1290 } else { 1291 _logPassed( 1292 prefix + this._taskSequence.length + ' tasks ran successfully.'); 1293 } 1294 1295 return Promise.resolve(); 1296 } 1297 1298 // |taskLabel| can be either a string or a dictionary. See Task constructor 1299 // for the detail. If |taskFunction| returns a thenable, then the task 1300 // is considered complete when the thenable is fulfilled; otherwise the 1301 // task must be completed with an explicit call to |task.done()|. 1302 define(taskLabel, taskFunction) { 1303 let task = new Task(this, taskLabel, taskFunction); 1304 if (this._tasks.hasOwnProperty(task.label)) { 1305 _throwException('Audit.define:: Duplicate task definition.'); 1306 return; 1307 } 1308 this._tasks[task.label] = task; 1309 this._taskSequence.push(task.label); 1310 } 1311 1312 // Start running all the tasks scheduled. Multiple task names can be passed 1313 // to execute them sequentially. Zero argument will perform all defined 1314 // tasks in the order of definition. 1315 run() { 1316 // Display the beginning of the test suite. 1317 _logPassed('# AUDIT TASK RUNNER STARTED.'); 1318 1319 // If the argument is specified, override the default task sequence with 1320 // the specified one. 1321 if (arguments.length > 0) { 1322 this._taskSequence = []; 1323 for (let i = 0; i < arguments.length; i++) { 1324 let taskLabel = arguments[i]; 1325 if (!this._tasks.hasOwnProperty(taskLabel)) { 1326 _throwException('Audit.run:: undefined task.'); 1327 } else if (this._taskSequence.includes(taskLabel)) { 1328 _throwException('Audit.run:: duplicate task request.'); 1329 } else { 1330 this._taskSequence.push(taskLabel); 1331 } 1332 } 1333 } 1334 1335 if (this._taskSequence.length === 0) { 1336 _throwException('Audit.run:: no task to run.'); 1337 return; 1338 } 1339 1340 for (let taskIndex in this._taskSequence) { 1341 let task = this._tasks[this._taskSequence[taskIndex]]; 1342 // Some tests assume that tasks run in sequence, which is provided by 1343 // promise_test(). 1344 promise_test((t) => task.run(t), `Executing "${task.label}"`); 1345 } 1346 1347 // Schedule a summary report on completion. 1348 promise_test(() => this._finish(), "Audit report"); 1349 1350 // From testharness.js. The harness now need not wait for more subtests 1351 // to be added. 1352 _testharnessDone(); 1353 } 1354 } 1355 1356 /** 1357 * Load file from a given URL and pass ArrayBuffer to the following promise. 1358 * @param {String} fileUrl file URL. 1359 * @return {Promise} 1360 * 1361 * @example 1362 * Audit.loadFileFromUrl('resources/my-sound.ogg').then((response) => { 1363 * audioContext.decodeAudioData(response).then((audioBuffer) => { 1364 * // Do something with AudioBuffer. 1365 * }); 1366 * }); 1367 */ 1368 function loadFileFromUrl(fileUrl) { 1369 return new Promise((resolve, reject) => { 1370 let xhr = new XMLHttpRequest(); 1371 xhr.open('GET', fileUrl, true); 1372 xhr.responseType = 'arraybuffer'; 1373 1374 xhr.onload = () => { 1375 // |status = 0| is a workaround for the run_web_test.py server. We are 1376 // speculating the server quits the transaction prematurely without 1377 // completing the request. 1378 if (xhr.status === 200 || xhr.status === 0) { 1379 resolve(xhr.response); 1380 } else { 1381 let errorMessage = 'loadFile: Request failed when loading ' + 1382 fileUrl + '. ' + xhr.statusText + '. (status = ' + xhr.status + 1383 ')'; 1384 if (reject) { 1385 reject(errorMessage); 1386 } else { 1387 new Error(errorMessage); 1388 } 1389 } 1390 }; 1391 1392 xhr.onerror = (event) => { 1393 let errorMessage = 1394 'loadFile: Network failure when loading ' + fileUrl + '.'; 1395 if (reject) { 1396 reject(errorMessage); 1397 } else { 1398 new Error(errorMessage); 1399 } 1400 }; 1401 1402 xhr.send(); 1403 }); 1404 } 1405 1406 /** 1407 * @class Audit 1408 * @description A WebAudio layout test task manager. 1409 * @example 1410 * let audit = Audit.createTaskRunner(); 1411 * audit.define('first-task', function (task, should) { 1412 * should(someValue).beEqualTo(someValue); 1413 * task.done(); 1414 * }); 1415 * audit.run(); 1416 */ 1417 return { 1418 1419 /** 1420 * Creates an instance of Audit task runner. 1421 * @param {Object} options Options for task runner. 1422 * @param {Boolean} options.requireResultFile True if the test suite 1423 * requires explicit text 1424 * comparison with the expected 1425 * result file. 1426 */ 1427 createTaskRunner: function(options) { 1428 if (options && options.requireResultFile == true) { 1429 _logError( 1430 'this test requires the explicit comparison with the ' + 1431 'expected result when it runs with run_web_tests.py.'); 1432 } 1433 1434 return new TaskRunner(); 1435 }, 1436 1437 /** 1438 * Load file from a given URL and pass ArrayBuffer to the following promise. 1439 * See |loadFileFromUrl| method for the detail. 1440 */ 1441 loadFileFromUrl: loadFileFromUrl 1442 1443 }; 1444 1445 })();