webgl-test-harness.js (20813B)
1 /* 2 Copyright (c) 2019 The Khronos Group Inc. 3 Use of this source code is governed by an MIT-style license that can be 4 found in the LICENSE.txt file. 5 */ 6 7 // This is a test harness for running javascript tests in the browser. 8 // The only identifier exposed by this harness is WebGLTestHarnessModule. 9 // 10 // To use it make an HTML page with an iframe. Then call the harness like this 11 // 12 // function reportResults(type, msg, success) { 13 // ... 14 // return true; 15 // } 16 // 17 // var fileListURL = '00_test_list.txt'; 18 // var testHarness = new WebGLTestHarnessModule.TestHarness( 19 // iframe, 20 // fileListURL, 21 // reportResults, 22 // options); 23 // 24 // The harness will load the fileListURL and parse it for the URLs, one URL 25 // per line preceded by options, see below. URLs should be on the same domain 26 // and at the same folder level or below the main html file. If any URL ends 27 // in .txt it will be parsed as well so you can nest .txt files. URLs inside a 28 // .txt file should be relative to that text file. 29 // 30 // During startup, for each page found the reportFunction will be called with 31 // WebGLTestHarnessModule.TestHarness.reportType.ADD_PAGE and msg will be 32 // the URL of the test. 33 // 34 // Each test is required to call testHarness.reportResults. This is most easily 35 // accomplished by storing that value on the main window with 36 // 37 // window.webglTestHarness = testHarness 38 // 39 // and then adding these to functions to your tests. 40 // 41 // function reportTestResultsToHarness(success, msg) { 42 // if (window.parent.webglTestHarness) { 43 // window.parent.webglTestHarness.reportResults(success, msg); 44 // } 45 // } 46 // 47 // function notifyFinishedToHarness() { 48 // if (window.parent.webglTestHarness) { 49 // window.parent.webglTestHarness.notifyFinished(); 50 // } 51 // } 52 // 53 // This way your tests will still run without the harness and you can use 54 // any testing framework you want. 55 // 56 // Each test should call reportTestResultsToHarness with true for success if it 57 // succeeded and false if it fail followed and any message it wants to 58 // associate with the test. If your testing framework supports checking for 59 // timeout you can call it with success equal to undefined in that case. 60 // 61 // To run the tests, call testHarness.runTests(options); 62 // 63 // For each test run, before the page is loaded the reportFunction will be 64 // called with WebGLTestHarnessModule.TestHarness.reportType.START_PAGE and msg 65 // will be the URL of the test. You may return false if you want the test to be 66 // skipped. 67 // 68 // For each test completed the reportFunction will be called with 69 // with WebGLTestHarnessModule.TestHarness.reportType.TEST_RESULT, 70 // success = true on success, false on failure, undefined on timeout 71 // and msg is any message the test choose to pass on. 72 // 73 // When all the tests on the page have finished your page must call 74 // notifyFinishedToHarness. If notifyFinishedToHarness is not called 75 // the harness will assume the test timed out. 76 // 77 // When all the tests on a page have finished OR the page as timed out the 78 // reportFunction will be called with 79 // WebGLTestHarnessModule.TestHarness.reportType.FINISH_PAGE 80 // where success = true if the page has completed or undefined if the page timed 81 // out. 82 // 83 // Finally, when all the tests have completed the reportFunction will be called 84 // with WebGLTestHarnessModule.TestHarness.reportType.FINISHED_ALL_TESTS. 85 // 86 // Harness Options 87 // 88 // These are passed in to the TestHarness as a JavaScript object 89 // 90 // version: (required!) 91 // 92 // Specifies a version used to filter tests. Tests marked as requiring 93 // a version greater than this version will not be included. 94 // 95 // example: new TestHarness(...., {version: "3.1.2"}); 96 // 97 // minVersion: 98 // 99 // Specifies the minimum version a test must require to be included. 100 // This basically flips the filter so that only tests marked with 101 // --min-version will be included if they are at this minVersion or 102 // greater. 103 // 104 // example: new TestHarness(...., {minVersion: "2.3.1"}); 105 // 106 // maxVersion: 107 // 108 // Specifies the maximum version a test must require to be included. 109 // This basically flips the filter so that only tests marked with 110 // --max-version will be included if they are at this maxVersion or 111 // less. 112 // 113 // example: new TestHarness(...., {maxVersion: "2.3.1"}); 114 // 115 // fast: 116 // 117 // Specifies to skip any tests marked as slow. 118 // 119 // example: new TestHarness(..., {fast: true}); 120 // 121 // Test Options: 122 // 123 // Any test URL or .txt file can be prefixed by the following options 124 // 125 // min-version: 126 // 127 // Sets the minimum version required to include this test. A version is 128 // passed into the harness options. Any test marked as requiring a 129 // min-version greater than the version passed to the harness is skipped. 130 // This allows you to add new tests to a suite of tests for a future 131 // version of the suite without including the test in the current version. 132 // If no -min-version is specified it is inheriited from the .txt file 133 // including it. The default is 1.0.0 134 // 135 // example: --min-version 2.1.3 sometest.html 136 // 137 // max-version: 138 // 139 // Sets the maximum version required to include this test. A version is 140 // passed into the harness options. Any test marked as requiring a 141 // max-version less than the version passed to the harness is skipped. 142 // This allows you to test functionality that has been removed from later 143 // versions of the suite. 144 // If no -max-version is specified it is inherited from the .txt file 145 // including it. 146 // 147 // example: --max-version 1.9.9 sometest.html 148 // 149 // slow: 150 // 151 // Marks a test as slow. Slow tests can be skipped by passing fastOnly: true 152 // to the TestHarness. Of course you need to pass all tests but sometimes 153 // you'd like to test quickly and run only the fast subset of tests. 154 // 155 // example: --slow some-test-that-takes-2-mins.html 156 // 157 158 WebGLTestHarnessModule = function() { 159 160 /** 161 * Wrapped logging function. 162 */ 163 var log = function(msg) { 164 if (window.console && window.console.log) { 165 window.console.log(msg); 166 } 167 }; 168 169 /** 170 * Loads text from an external file. This function is synchronous. 171 * @param {string} url The url of the external file. 172 * @param {!function(bool, string): void} callback that is sent a bool for 173 * success and the string. 174 */ 175 var loadTextFileAsynchronous = function(url, callback) { 176 log ("loading: " + url); 177 var error = 'loadTextFileSynchronous failed to load url "' + url + '"'; 178 var request; 179 if (window.XMLHttpRequest) { 180 request = new XMLHttpRequest(); 181 if (request.overrideMimeType) { 182 request.overrideMimeType('text/plain'); 183 } 184 } else { 185 throw 'XMLHttpRequest is disabled'; 186 } 187 try { 188 request.open('GET', url, true); 189 request.onreadystatechange = function() { 190 if (request.readyState == 4) { 191 var text = ''; 192 // HTTP reports success with a 200 status. The file protocol reports 193 // success with zero. HTTP does not use zero as a status code (they 194 // start at 100). 195 // https://developer.mozilla.org/En/Using_XMLHttpRequest 196 var success = request.status == 200 || request.status == 0; 197 if (success) { 198 text = request.responseText; 199 } 200 log("loaded: " + url); 201 callback(success, text); 202 } 203 }; 204 request.send(null); 205 } catch (e) { 206 log("failed to load: " + url); 207 callback(false, ''); 208 } 209 }; 210 211 /** 212 * @param {string} versionString WebGL version string. 213 * @return {number} Integer containing the WebGL major version. 214 */ 215 var getMajorVersion = function(versionString) { 216 if (!versionString) { 217 return 1; 218 } 219 return parseInt(versionString.split(" ")[0].split(".")[0], 10); 220 }; 221 222 /** 223 * @param {string} url Base URL of the test. 224 * @param {map} options Map of options to append to the URL's query string. 225 * @return {string} URL that will run the test with the given WebGL version. 226 */ 227 var getURLWithOptions = function(url, options) { 228 var queryArgs = 0; 229 230 for (i in options) { 231 url += queryArgs ? "&" : "?"; 232 url += i + "=" + options[i]; 233 queryArgs++; 234 } 235 236 return url; 237 }; 238 239 /** 240 * Compare version strings. 241 */ 242 var greaterThanOrEqualToVersion = function(have, want) { 243 have = have.split(" ")[0].split("."); 244 want = want.split(" ")[0].split("."); 245 246 //have 1.2.3 want 1.1 247 //have 1.1.1 want 1.1 248 //have 1.0.9 want 1.1 249 //have 1.1 want 1.1.1 250 251 for (var ii = 0; ii < want.length; ++ii) { 252 var wantNum = parseInt(want[ii]); 253 var haveNum = have[ii] ? parseInt(have[ii]) : 0 254 if (haveNum > wantNum) { 255 return true; // 2.0.0 is greater than 1.2.3 256 } 257 if (haveNum < wantNum) { 258 return false; 259 } 260 } 261 return true; 262 }; 263 264 /** 265 * Reads a file, recursively adding files referenced inside. 266 * 267 * Each line of URL is parsed, comments starting with '#' or ';' 268 * or '//' are stripped. 269 * 270 * arguments beginning with -- are extracted 271 * 272 * lines that end in .txt are recursively scanned for more files 273 * other lines are added to the list of files. 274 * 275 * @param {string} url The url of the file to read. 276 * @param {function(boolean, !Array.<string>):void} callback 277 * Callback that is called with true for success and an 278 * array of filenames. 279 * @param {Object} options Optional options 280 * 281 * Options: 282 * version: {string} The version of the conformance test. 283 * Tests with the argument --min-version <version> will 284 * be ignored version is less then <version> 285 * 286 */ 287 var getFileList = function(url, callback, options) { 288 var files = []; 289 290 var copyObject = function(obj) { 291 return JSON.parse(JSON.stringify(obj)); 292 }; 293 294 var toCamelCase = function(str) { 295 return str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase() }); 296 }; 297 298 var globalOptions = copyObject(options); 299 globalOptions.defaultVersion = "1.0"; 300 globalOptions.defaultMaxVersion = null; 301 302 var getFileListImpl = function(prefix, line, lineNum, hierarchicalOptions, callback) { 303 var files = []; 304 305 var args = line.split(/\s+/); 306 var nonOptions = []; 307 var useTest = true; 308 var testOptions = {}; 309 for (var jj = 0; jj < args.length; ++jj) { 310 var arg = args[jj]; 311 if (arg[0] == '-') { 312 if (arg[1] != '-') { 313 throw ("bad option at in " + url + ":" + lineNum + ": " + arg); 314 } 315 var option = arg.substring(2); 316 switch (option) { 317 // no argument options. 318 case 'slow': 319 testOptions[toCamelCase(option)] = true; 320 break; 321 // one argument options. 322 case 'min-version': 323 case 'max-version': 324 ++jj; 325 testOptions[toCamelCase(option)] = args[jj]; 326 break; 327 default: 328 throw ("bad unknown option '" + option + "' at in " + url + ":" + lineNum + ": " + arg); 329 } 330 } else { 331 nonOptions.push(arg); 332 } 333 } 334 var url = prefix + nonOptions.join(" "); 335 336 if (url.substr(url.length - 4) != '.txt') { 337 var minVersion = testOptions.minVersion; 338 if (!minVersion) { 339 minVersion = hierarchicalOptions.defaultVersion; 340 } 341 var maxVersion = testOptions.maxVersion; 342 if (!maxVersion) { 343 maxVersion = hierarchicalOptions.defaultMaxVersion; 344 } 345 var slow = testOptions.slow; 346 if (!slow) { 347 slow = hierarchicalOptions.defaultSlow; 348 } 349 350 if (globalOptions.fast && slow) { 351 useTest = false; 352 } else if (globalOptions.minVersion) { 353 useTest = greaterThanOrEqualToVersion(minVersion, globalOptions.minVersion); 354 } else if (globalOptions.maxVersion && maxVersion) { 355 useTest = greaterThanOrEqualToVersion(globalOptions.maxVersion, maxVersion); 356 } else { 357 useTest = greaterThanOrEqualToVersion(globalOptions.version, minVersion); 358 if (maxVersion) { 359 useTest = useTest && greaterThanOrEqualToVersion(maxVersion, globalOptions.version); 360 } 361 } 362 } 363 364 if (!useTest) { 365 callback(true, []); 366 return; 367 } 368 369 if (url.substr(url.length - 4) == '.txt') { 370 // If a version was explicity specified pass it down. 371 if (testOptions.minVersion) { 372 hierarchicalOptions.defaultVersion = testOptions.minVersion; 373 } 374 if (testOptions.maxVersion) { 375 hierarchicalOptions.defaultMaxVersion = testOptions.maxVersion; 376 } 377 if (testOptions.slow) { 378 hierarchicalOptions.defaultSlow = testOptions.slow; 379 } 380 loadTextFileAsynchronous(url, function() { 381 return function(success, text) { 382 if (!success) { 383 callback(false, ''); 384 return; 385 } 386 var lines = text.split('\n'); 387 var prefix = ''; 388 var lastSlash = url.lastIndexOf('/'); 389 if (lastSlash >= 0) { 390 prefix = url.substr(0, lastSlash + 1); 391 } 392 var fail = false; 393 var count = 1; 394 var index = 0; 395 for (var ii = 0; ii < lines.length; ++ii) { 396 var str = lines[ii].replace(/^\s\s*/, '').replace(/\s\s*$/, ''); 397 if (str.length > 4 && 398 str[0] != '#' && 399 str[0] != ";" && 400 str.substr(0, 2) != "//") { 401 ++count; 402 getFileListImpl(prefix, str, ii + 1, copyObject(hierarchicalOptions), function(index) { 403 return function(success, new_files) { 404 //log("got files: " + new_files.length); 405 if (success) { 406 files[index] = new_files; 407 } 408 finish(success); 409 }; 410 }(index++)); 411 } 412 } 413 finish(true); 414 415 function finish(success) { 416 if (!success) { 417 fail = true; 418 } 419 --count; 420 //log("count: " + count); 421 if (!count) { 422 callback(!fail, files); 423 } 424 } 425 } 426 }()); 427 } else { 428 files.push(url); 429 callback(true, files); 430 } 431 }; 432 433 getFileListImpl('', url, 1, globalOptions, function(success, files) { 434 // flatten 435 var flat = []; 436 flatten(files); 437 function flatten(files) { 438 for (var ii = 0; ii < files.length; ++ii) { 439 var value = files[ii]; 440 if (typeof(value) == "string") { 441 flat.push(value); 442 } else { 443 flatten(value); 444 } 445 } 446 } 447 callback(success, flat); 448 }); 449 }; 450 451 var FilterURL = (function() { 452 var prefix = window.location.pathname; 453 prefix = prefix.substring(0, prefix.lastIndexOf("/") + 1); 454 return function(url) { 455 if (url.substring(0, prefix.length) == prefix) { 456 url = url.substring(prefix.length); 457 } 458 return url; 459 }; 460 }()); 461 462 var TestFile = function(url) { 463 this.url = url; 464 }; 465 466 var Test = function(file) { 467 this.file = file; 468 }; 469 470 var TestHarness = function(iframe, filelistUrl, reportFunc, options) { 471 this.window = window; 472 this.iframes = iframe.length ? iframe : [iframe]; 473 this.reportFunc = reportFunc; 474 this.timeoutDelay = 20000; 475 this.files = []; 476 this.allowSkip = options.allowSkip; 477 this.webglVersion = getMajorVersion(options.version); 478 this.dumpShaders = options.dumpShaders; 479 this.quiet = options.quiet; 480 481 var that = this; 482 getFileList(filelistUrl, function() { 483 return function(success, files) { 484 that.addFiles_(success, files); 485 }; 486 }(), options); 487 488 }; 489 490 TestHarness.reportType = { 491 ADD_PAGE: 1, 492 READY: 2, 493 START_PAGE: 3, 494 TEST_RESULT: 4, 495 FINISH_PAGE: 5, 496 FINISHED_ALL_TESTS: 6 497 }; 498 499 TestHarness.prototype.addFiles_ = function(success, files) { 500 if (!success) { 501 this.reportFunc( 502 TestHarness.reportType.FINISHED_ALL_TESTS, 503 '', 504 'Unable to load tests. Are you running locally?\n' + 505 'You need to run from a server or configure your\n' + 506 'browser to allow access to local files (not recommended).\n\n' + 507 'Note: An easy way to run from a server:\n\n' + 508 '\tcd path_to_tests\n' + 509 '\tpython -m SimpleHTTPServer\n\n' + 510 'then point your browser to ' + 511 '<a href="http://localhost:8000/webgl-conformance-tests.html">' + 512 'http://localhost:8000/webgl-conformance-tests.html</a>', 513 false) 514 return; 515 } 516 log("total files: " + files.length); 517 for (var ii = 0; ii < files.length; ++ii) { 518 log("" + ii + ": " + files[ii]); 519 this.files.push(new TestFile(files[ii])); 520 this.reportFunc(TestHarness.reportType.ADD_PAGE, '', files[ii], undefined); 521 } 522 this.reportFunc(TestHarness.reportType.READY, '', undefined, undefined); 523 } 524 525 TestHarness.prototype.runTests = function(opt_options) { 526 var options = opt_options || { }; 527 options.start = options.start || 0; 528 options.count = options.count || this.files.length; 529 530 this.idleIFrames = this.iframes.slice(0); 531 this.runningTests = {}; 532 var testsToRun = []; 533 for (var ii = 0; ii < options.count; ++ii) { 534 testsToRun.push(ii + options.start); 535 } 536 this.numTestsRemaining = options.count; 537 this.testsToRun = testsToRun; 538 this.startNextTest(); 539 }; 540 541 TestHarness.prototype._bumpTimeout = function(test) { 542 const newTimeoutAt = performance.now() + this.timeoutDelay; 543 if (test.timeoutAt) { 544 test.timeoutAt = newTimeoutAt; 545 return; 546 } 547 test.timeoutAt = newTimeoutAt; 548 549 const harness = this; 550 551 function enqueueWatchdog() { 552 const remaining = test.timeoutAt - performance.now(); 553 //console.log(`watchdog started at ${performance.now()}, ${test.timeoutAt} requested`); 554 this.window.setTimeout(() => { 555 if (!test.timeoutAt) return; // Timeout was cleared. 556 const remainingAtCheckTime = test.timeoutAt - performance.now(); 557 if (performance.now() >= test.timeoutAt) { 558 //console.log(`watchdog won at ${performance.now()}, ${test.timeoutAt} requested`); 559 harness.timeout(test); 560 return; 561 } 562 //console.log(`watchdog lost at ${performance.now()}, as ${test.timeoutAt} is now requested`); 563 enqueueWatchdog(); 564 }, remaining); 565 } 566 enqueueWatchdog(); 567 }; 568 569 TestHarness.prototype.clearTimeout = function(test) { 570 test.timeoutAt = null; 571 }; 572 573 TestHarness.prototype.startNextTest = function() { 574 if (this.numTestsRemaining == 0) { 575 log("done"); 576 this.reportFunc(TestHarness.reportType.FINISHED_ALL_TESTS, 577 '', '', true); 578 } else { 579 while (this.testsToRun.length > 0 && this.idleIFrames.length > 0) { 580 var testId = this.testsToRun.shift(); 581 var iframe = this.idleIFrames.shift(); 582 this.startTest(iframe, this.files[testId], this.webglVersion); 583 } 584 } 585 }; 586 587 TestHarness.prototype.startTest = function(iframe, testFile, webglVersion) { 588 var test = { 589 iframe: iframe, 590 testFile: testFile 591 }; 592 var url = testFile.url; 593 this.runningTests[url] = test; 594 log("loading: " + url); 595 if (this.reportFunc(TestHarness.reportType.START_PAGE, url, url, undefined)) { 596 iframe.src = getURLWithOptions(url, { 597 "webglVersion": webglVersion, 598 "dumpShaders": this.dumpShaders, 599 "quiet": this.quiet 600 }); 601 this._bumpTimeout(test); 602 } else { 603 this.reportResults(url, !!this.allowSkip, "skipped", true); 604 this.notifyFinished(url); 605 } 606 }; 607 608 TestHarness.prototype.getTest = function(url) { 609 var test = this.runningTests[FilterURL(url)]; 610 if (!test) { 611 throw("unknown test:" + url); 612 } 613 return test; 614 }; 615 616 TestHarness.prototype.reportResults = function(url, success, msg, skipped) { 617 url = FilterURL(url); 618 var test = this.getTest(url); 619 if (0) { 620 // This is too slow to leave on for tests like 621 // deqp/functional/gles3/vertexarrays/multiple_attributes.output.html 622 // which has 33013505 calls to reportResults. 623 log((success ? "PASS" : "FAIL") + ": " + msg); 624 } 625 this.reportFunc(TestHarness.reportType.TEST_RESULT, url, msg, success, skipped); 626 // For each result we get, reset the timeout 627 this._bumpTimeout(test); 628 }; 629 630 TestHarness.prototype.dequeTest = function(test) { 631 this.clearTimeout(test); 632 this.idleIFrames.push(test.iframe); 633 delete this.runningTests[test.testFile.url]; 634 --this.numTestsRemaining; 635 } 636 637 TestHarness.prototype.notifyFinished = function(url) { 638 url = FilterURL(url); 639 var test = this.getTest(url); 640 log(url + ": finished"); 641 this.dequeTest(test); 642 this.reportFunc(TestHarness.reportType.FINISH_PAGE, url, url, true); 643 this.startNextTest(); 644 }; 645 646 TestHarness.prototype.timeout = function(test) { 647 this.dequeTest(test); 648 var url = test.testFile.url; 649 log(url + ": timeout"); 650 this.reportFunc(TestHarness.reportType.FINISH_PAGE, url, url, undefined); 651 this.startNextTest(); 652 }; 653 654 TestHarness.prototype.setTimeoutDelay = function(x) { 655 this.timeoutDelay = x; 656 }; 657 658 return { 659 'TestHarness': TestHarness, 660 'getMajorVersion': getMajorVersion, 661 'getURLWithOptions': getURLWithOptions 662 }; 663 664 }();