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.
7 // This is a test harness for running javascript tests in the browser.
8 // The only identifier exposed by this harness is WebGLTestHarnessModule.
10 // To use it make an HTML page with an iframe. Then call the harness like this
12 // function reportResults(type, msg, success) {
17 // var fileListURL = '00_test_list.txt';
18 // var testHarness = new WebGLTestHarnessModule.TestHarness(
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.
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.
34 // Each test is required to call testHarness.reportResults. This is most easily
35 // accomplished by storing that value on the main window with
37 // window.webglTestHarness = testHarness
39 // and then adding these to functions to your tests.
41 // function reportTestResultsToHarness(success, msg) {
42 // if (window.parent.webglTestHarness) {
43 // window.parent.webglTestHarness.reportResults(success, msg);
47 // function notifyFinishedToHarness() {
48 // if (window.parent.webglTestHarness) {
49 // window.parent.webglTestHarness.notifyFinished();
53 // This way your tests will still run without the harness and you can use
54 // any testing framework you want.
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.
61 // To run the tests, call testHarness.runTests(options);
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
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.
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.
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
83 // Finally, when all the tests have completed the reportFunction will be called
84 // with WebGLTestHarnessModule.TestHarness.reportType.FINISHED_ALL_TESTS.
88 // These are passed in to the TestHarness as a JavaScript object
90 // version: (required!)
92 // Specifies a version used to filter tests. Tests marked as requiring
93 // a version greater than this version will not be included.
95 // example: new TestHarness(...., {version: "3.1.2"});
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
104 // example: new TestHarness(...., {minVersion: "2.3.1"});
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
113 // example: new TestHarness(...., {maxVersion: "2.3.1"});
117 // Specifies to skip any tests marked as slow.
119 // example: new TestHarness(..., {fast: true});
123 // Any test URL or .txt file can be prefixed by the following options
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
135 // example: --min-version 2.1.3 sometest.html
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
147 // example: --max-version 1.9.9 sometest.html
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.
155 // example: --slow some-test-that-takes-2-mins.html
158 WebGLTestHarnessModule = function() {
161 * Wrapped logging function.
163 var log = function(msg) {
164 if (window.console && window.console.log) {
165 window.console.log(msg);
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.
175 var loadTextFileAsynchronous = function(url, callback) {
176 log ("loading: " + url);
177 var error = 'loadTextFileSynchronous failed to load url "' + url + '"';
179 if (window.XMLHttpRequest) {
180 request = new XMLHttpRequest();
181 if (request.overrideMimeType) {
182 request.overrideMimeType('text/plain');
185 throw 'XMLHttpRequest is disabled';
188 request.open('GET', url, true);
189 request.onreadystatechange = function() {
190 if (request.readyState == 4) {
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
195 // https://developer.mozilla.org/En/Using_XMLHttpRequest
196 var success = request.status == 200 || request.status == 0;
198 text = request.responseText;
200 log("loaded: " + url);
201 callback(success, text);
206 log("failed to load: " + url);
212 * @param {string} versionString WebGL version string.
213 * @return {number} Integer containing the WebGL major version.
215 var getMajorVersion = function(versionString) {
216 if (!versionString) {
219 return parseInt(versionString.split(" ")[0].split(".")[0], 10);
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.
227 var getURLWithOptions = function(url, options) {
231 url += queryArgs ? "&" : "?";
232 url += i + "=" + options[i];
240 * Compare version strings.
242 var greaterThanOrEqualToVersion = function(have, want) {
243 have = have.split(" ")[0].split(".");
244 want = want.split(" ")[0].split(".");
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
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
257 if (haveNum < wantNum) {
265 * Reads a file, recursively adding files referenced inside.
267 * Each line of URL is parsed, comments starting with '#' or ';'
268 * or '//' are stripped.
270 * arguments beginning with -- are extracted
272 * lines that end in .txt are recursively scanned for more files
273 * other lines are added to the list of files.
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
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>
287 var getFileList = function(url, callback, options) {
290 var copyObject = function(obj) {
291 return JSON.parse(JSON.stringify(obj));
294 var toCamelCase = function(str) {
295 return str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase() });
298 var globalOptions = copyObject(options);
299 globalOptions.defaultVersion = "1.0";
300 globalOptions.defaultMaxVersion = null;
302 var getFileListImpl = function(prefix, line, lineNum, hierarchicalOptions, callback) {
305 var args = line.split(/\s+/);
308 var testOptions = {};
309 for (var jj = 0; jj < args.length; ++jj) {
313 throw ("bad option at in " + url + ":" + lineNum + ": " + arg);
315 var option = arg.substring(2);
317 // no argument options.
319 testOptions[toCamelCase(option)] = true;
321 // one argument options.
325 testOptions[toCamelCase(option)] = args[jj];
328 throw ("bad unknown option '" + option + "' at in " + url + ":" + lineNum + ": " + arg);
331 nonOptions.push(arg);
334 var url = prefix + nonOptions.join(" ");
336 if (url.substr(url.length - 4) != '.txt') {
337 var minVersion = testOptions.minVersion;
339 minVersion = hierarchicalOptions.defaultVersion;
341 var maxVersion = testOptions.maxVersion;
343 maxVersion = hierarchicalOptions.defaultMaxVersion;
345 var slow = testOptions.slow;
347 slow = hierarchicalOptions.defaultSlow;
350 if (globalOptions.fast && slow) {
352 } else if (globalOptions.minVersion) {
353 useTest = greaterThanOrEqualToVersion(minVersion, globalOptions.minVersion);
354 } else if (globalOptions.maxVersion && maxVersion) {
355 useTest = greaterThanOrEqualToVersion(globalOptions.maxVersion, maxVersion);
357 useTest = greaterThanOrEqualToVersion(globalOptions.version, minVersion);
359 useTest = useTest && greaterThanOrEqualToVersion(maxVersion, globalOptions.version);
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;
374 if (testOptions.maxVersion) {
375 hierarchicalOptions.defaultMaxVersion = testOptions.maxVersion;
377 if (testOptions.slow) {
378 hierarchicalOptions.defaultSlow = testOptions.slow;
380 loadTextFileAsynchronous(url, function() {
381 return function(success, text) {
386 var lines = text.split('\n');
388 var lastSlash = url.lastIndexOf('/');
389 if (lastSlash >= 0) {
390 prefix = url.substr(0, lastSlash + 1);
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 &&
400 str.substr(0, 2) != "//") {
402 getFileListImpl(prefix, str, ii + 1, copyObject(hierarchicalOptions), function(index) {
403 return function(success, new_files) {
404 //log("got files: " + new_files.length);
406 files[index] = new_files;
415 function finish(success) {
420 //log("count: " + count);
422 callback(!fail, files);
429 callback(true, files);
433 getFileListImpl('', url, 1, globalOptions, function(success, files) {
437 function flatten(files) {
438 for (var ii = 0; ii < files.length; ++ii) {
439 var value = files[ii];
440 if (typeof(value) == "string") {
447 callback(success, flat);
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);
462 var TestFile = function(url) {
466 var Test = function(file) {
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;
476 this.allowSkip = options.allowSkip;
477 this.webglVersion = getMajorVersion(options.version);
478 this.dumpShaders = options.dumpShaders;
479 this.quiet = options.quiet;
482 getFileList(filelistUrl, function() {
483 return function(success, files) {
484 that.addFiles_(success, files);
490 TestHarness.reportType = {
496 FINISHED_ALL_TESTS: 6
499 TestHarness.prototype.addFiles_ = function(success, files) {
502 TestHarness.reportType.FINISHED_ALL_TESTS,
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>',
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);
522 this.reportFunc(TestHarness.reportType.READY, '', undefined, undefined);
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;
530 this.idleIFrames = this.iframes.slice(0);
531 this.runningTests = {};
533 for (var ii = 0; ii < options.count; ++ii) {
534 testsToRun.push(ii + options.start);
536 this.numTestsRemaining = options.count;
537 this.testsToRun = testsToRun;
538 this.startNextTest();
541 TestHarness.prototype._bumpTimeout = function(test) {
542 const newTimeoutAt = performance.now() + this.timeoutDelay;
543 if (test.timeoutAt) {
544 test.timeoutAt = newTimeoutAt;
547 test.timeoutAt = newTimeoutAt;
549 const harness = this;
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);
562 //console.log(`watchdog lost at ${performance.now()}, as ${test.timeoutAt} is now requested`);
569 TestHarness.prototype.clearTimeout = function(test) {
570 test.timeoutAt = null;
573 TestHarness.prototype.startNextTest = function() {
574 if (this.numTestsRemaining == 0) {
576 this.reportFunc(TestHarness.reportType.FINISHED_ALL_TESTS,
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);
587 TestHarness.prototype.startTest = function(iframe, testFile, webglVersion) {
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,
601 this._bumpTimeout(test);
603 this.reportResults(url, !!this.allowSkip, "skipped", true);
604 this.notifyFinished(url);
608 TestHarness.prototype.getTest = function(url) {
609 var test = this.runningTests[FilterURL(url)];
611 throw("unknown test:" + url);
616 TestHarness.prototype.reportResults = function(url, success, msg, skipped) {
617 url = FilterURL(url);
618 var test = this.getTest(url);
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);
625 this.reportFunc(TestHarness.reportType.TEST_RESULT, url, msg, success, skipped);
626 // For each result we get, reset the timeout
627 this._bumpTimeout(test);
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;
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();
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();
654 TestHarness.prototype.setTimeoutDelay = function(x) {
655 this.timeoutDelay = x;
659 'TestHarness': TestHarness,
660 'getMajorVersion': getMajorVersion,
661 'getURLWithOptions': getURLWithOptions