| /* |
| ** Copyright (c) 2012 The Khronos Group Inc. |
| ** |
| ** Permission is hereby granted, free of charge, to any person obtaining a |
| ** copy of this software and/or associated documentation files (the |
| ** "Materials"), to deal in the Materials without restriction, including |
| ** without limitation the rights to use, copy, modify, merge, publish, |
| ** distribute, sublicense, and/or sell copies of the Materials, and to |
| ** permit persons to whom the Materials are furnished to do so, subject to |
| ** the following conditions: |
| ** |
| ** The above copyright notice and this permission notice shall be included |
| ** in all copies or substantial portions of the Materials. |
| ** |
| ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
| ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
| ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. |
| */ |
| |
| // This is a test harness for running javascript tests in the browser. |
| // The only identifier exposed by this harness is WebGLTestHarnessModule. |
| // |
| // To use it make an HTML page with an iframe. Then call the harness like this |
| // |
| // function reportResults(type, msg, success) { |
| // ... |
| // return true; |
| // } |
| // |
| // var fileListURL = '00_test_list.txt'; |
| // var testHarness = new WebGLTestHarnessModule.TestHarness( |
| // iframe, |
| // fileListURL, |
| // reportResults, |
| // options); |
| // |
| // The harness will load the fileListURL and parse it for the URLs, one URL |
| // per line preceded by options, see below. URLs should be on the same domain |
| // and at the same folder level or below the main html file. If any URL ends |
| // in .txt it will be parsed as well so you can nest .txt files. URLs inside a |
| // .txt file should be relative to that text file. |
| // |
| // During startup, for each page found the reportFunction will be called with |
| // WebGLTestHarnessModule.TestHarness.reportType.ADD_PAGE and msg will be |
| // the URL of the test. |
| // |
| // Each test is required to call testHarness.reportResults. This is most easily |
| // accomplished by storing that value on the main window with |
| // |
| // window.webglTestHarness = testHarness |
| // |
| // and then adding these to functions to your tests. |
| // |
| // function reportTestResultsToHarness(success, msg) { |
| // if (window.parent.webglTestHarness) { |
| // window.parent.webglTestHarness.reportResults(success, msg); |
| // } |
| // } |
| // |
| // function notifyFinishedToHarness() { |
| // if (window.parent.webglTestHarness) { |
| // window.parent.webglTestHarness.notifyFinished(); |
| // } |
| // } |
| // |
| // This way your tests will still run without the harness and you can use |
| // any testing framework you want. |
| // |
| // Each test should call reportTestResultsToHarness with true for success if it |
| // succeeded and false if it fail followed and any message it wants to |
| // associate with the test. If your testing framework supports checking for |
| // timeout you can call it with success equal to undefined in that case. |
| // |
| // To run the tests, call testHarness.runTests(options); |
| // |
| // For each test run, before the page is loaded the reportFunction will be |
| // called with WebGLTestHarnessModule.TestHarness.reportType.START_PAGE and msg |
| // will be the URL of the test. You may return false if you want the test to be |
| // skipped. |
| // |
| // For each test completed the reportFunction will be called with |
| // with WebGLTestHarnessModule.TestHarness.reportType.TEST_RESULT, |
| // success = true on success, false on failure, undefined on timeout |
| // and msg is any message the test choose to pass on. |
| // |
| // When all the tests on the page have finished your page must call |
| // notifyFinishedToHarness. If notifyFinishedToHarness is not called |
| // the harness will assume the test timed out. |
| // |
| // When all the tests on a page have finished OR the page as timed out the |
| // reportFunction will be called with |
| // WebGLTestHarnessModule.TestHarness.reportType.FINISH_PAGE |
| // where success = true if the page has completed or undefined if the page timed |
| // out. |
| // |
| // Finally, when all the tests have completed the reportFunction will be called |
| // with WebGLTestHarnessModule.TestHarness.reportType.FINISHED_ALL_TESTS. |
| // |
| // Harness Options |
| // |
| // These are passed in to the TestHarness as a JavaScript object |
| // |
| // version: (required!) |
| // |
| // Specifies a version used to filter tests. Tests marked as requiring |
| // a version greater than this version will not be included. |
| // |
| // example: new TestHarness(...., {version: "3.1.2"}); |
| // |
| // minVersion: |
| // |
| // Specifies the minimum version a test must require to be included. |
| // This basically flips the filter so that only tests marked with |
| // --min-version will be included if they are at this minVersion or |
| // greater. |
| // |
| // example: new TestHarness(...., {minVersion: "2.3.1"}); |
| // |
| // maxVersion: |
| // |
| // Specifies the maximum version a test must require to be included. |
| // This basically flips the filter so that only tests marked with |
| // --max-version will be included if they are at this maxVersion or |
| // less. |
| // |
| // example: new TestHarness(...., {maxVersion: "2.3.1"}); |
| // |
| // fast: |
| // |
| // Specifies to skip any tests marked as slow. |
| // |
| // example: new TestHarness(..., {fast: true}); |
| // |
| // Test Options: |
| // |
| // Any test URL or .txt file can be prefixed by the following options |
| // |
| // min-version: |
| // |
| // Sets the minimum version required to include this test. A version is |
| // passed into the harness options. Any test marked as requiring a |
| // min-version greater than the version passed to the harness is skipped. |
| // This allows you to add new tests to a suite of tests for a future |
| // version of the suite without including the test in the current version. |
| // If no -min-version is specified it is inheriited from the .txt file |
| // including it. The default is 1.0.0 |
| // |
| // example: --min-version 2.1.3 sometest.html |
| // |
| // max-version: |
| // |
| // Sets the maximum version required to include this test. A version is |
| // passed into the harness options. Any test marked as requiring a |
| // max-version less than the version passed to the harness is skipped. |
| // This allows you to test functionality that has been removed from later |
| // versions of the suite. |
| // If no -max-version is specified it is inherited from the .txt file |
| // including it. |
| // |
| // example: --max-version 1.9.9 sometest.html |
| // |
| // slow: |
| // |
| // Marks a test as slow. Slow tests can be skipped by passing fastOnly: true |
| // to the TestHarness. Of course you need to pass all tests but sometimes |
| // you'd like to test quickly and run only the fast subset of tests. |
| // |
| // example: --slow some-test-that-takes-2-mins.html |
| // |
| |
| WebGLTestHarnessModule = function() { |
| |
| /** |
| * Wrapped logging function. |
| */ |
| var log = function(msg) { |
| if (window.console && window.console.log) { |
| window.console.log(msg); |
| } |
| }; |
| |
| /** |
| * Loads text from an external file. This function is synchronous. |
| * @param {string} url The url of the external file. |
| * @param {!function(bool, string): void} callback that is sent a bool for |
| * success and the string. |
| */ |
| var loadTextFileAsynchronous = function(url, callback) { |
| log ("loading: " + url); |
| var error = 'loadTextFileSynchronous failed to load url "' + url + '"'; |
| var request; |
| if (window.XMLHttpRequest) { |
| request = new XMLHttpRequest(); |
| if (request.overrideMimeType) { |
| request.overrideMimeType('text/plain'); |
| } |
| } else { |
| throw 'XMLHttpRequest is disabled'; |
| } |
| try { |
| request.open('GET', url, true); |
| request.onreadystatechange = function() { |
| if (request.readyState == 4) { |
| var text = ''; |
| // HTTP reports success with a 200 status. The file protocol reports |
| // success with zero. HTTP does not use zero as a status code (they |
| // start at 100). |
| // https://developer.mozilla.org/En/Using_XMLHttpRequest |
| var success = request.status == 200 || request.status == 0; |
| if (success) { |
| text = request.responseText; |
| } |
| log("loaded: " + url); |
| callback(success, text); |
| } |
| }; |
| request.send(null); |
| } catch (e) { |
| log("failed to load: " + url); |
| callback(false, ''); |
| } |
| }; |
| |
| /** |
| * @param {string} versionString WebGL version string. |
| * @return {number} Integer containing the WebGL major version. |
| */ |
| var getMajorVersion = function(versionString) { |
| if (!versionString) { |
| return 1; |
| } |
| return parseInt(versionString.split(" ")[0].split(".")[0], 10); |
| }; |
| |
| /** |
| * @param {string} url Base URL of the test. |
| * @param {map} options Map of options to append to the URL's query string. |
| * @return {string} URL that will run the test with the given WebGL version. |
| */ |
| var getURLWithOptions = function(url, options) { |
| var queryArgs = 0; |
| |
| for (i in options) { |
| url += queryArgs ? "&" : "?"; |
| url += i + "=" + options[i]; |
| queryArgs++; |
| } |
| |
| return url; |
| }; |
| |
| /** |
| * Compare version strings. |
| */ |
| var greaterThanOrEqualToVersion = function(have, want) { |
| have = have.split(" ")[0].split("."); |
| want = want.split(" ")[0].split("."); |
| |
| //have 1.2.3 want 1.1 |
| //have 1.1.1 want 1.1 |
| //have 1.0.9 want 1.1 |
| //have 1.1 want 1.1.1 |
| |
| for (var ii = 0; ii < want.length; ++ii) { |
| var wantNum = parseInt(want[ii]); |
| var haveNum = have[ii] ? parseInt(have[ii]) : 0 |
| if (haveNum > wantNum) { |
| return true; // 2.0.0 is greater than 1.2.3 |
| } |
| if (haveNum < wantNum) { |
| return false; |
| } |
| } |
| return true; |
| }; |
| |
| /** |
| * Reads a file, recursively adding files referenced inside. |
| * |
| * Each line of URL is parsed, comments starting with '#' or ';' |
| * or '//' are stripped. |
| * |
| * arguments beginning with -- are extracted |
| * |
| * lines that end in .txt are recursively scanned for more files |
| * other lines are added to the list of files. |
| * |
| * @param {string} url The url of the file to read. |
| * @param {function(boolean, !Array.<string>):void} callback |
| * Callback that is called with true for success and an |
| * array of filenames. |
| * @param {Object} options Optional options |
| * |
| * Options: |
| * version: {string} The version of the conformance test. |
| * Tests with the argument --min-version <version> will |
| * be ignored version is less then <version> |
| * |
| */ |
| var getFileList = function(url, callback, options) { |
| var files = []; |
| |
| var copyObject = function(obj) { |
| return JSON.parse(JSON.stringify(obj)); |
| }; |
| |
| var toCamelCase = function(str) { |
| return str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase() }); |
| }; |
| |
| var globalOptions = copyObject(options); |
| globalOptions.defaultVersion = "1.0"; |
| globalOptions.defaultMaxVersion = null; |
| |
| var getFileListImpl = function(prefix, line, lineNum, hierarchicalOptions, callback) { |
| var files = []; |
| |
| var args = line.split(/\s+/); |
| var nonOptions = []; |
| var useTest = true; |
| var testOptions = {}; |
| for (var jj = 0; jj < args.length; ++jj) { |
| var arg = args[jj]; |
| if (arg[0] == '-') { |
| if (arg[1] != '-') { |
| throw ("bad option at in " + url + ":" + lineNum + ": " + arg); |
| } |
| var option = arg.substring(2); |
| switch (option) { |
| // no argument options. |
| case 'slow': |
| testOptions[toCamelCase(option)] = true; |
| break; |
| // one argument options. |
| case 'min-version': |
| case 'max-version': |
| ++jj; |
| testOptions[toCamelCase(option)] = args[jj]; |
| break; |
| default: |
| throw ("bad unknown option '" + option + "' at in " + url + ":" + lineNum + ": " + arg); |
| } |
| } else { |
| nonOptions.push(arg); |
| } |
| } |
| var url = prefix + nonOptions.join(" "); |
| |
| if (url.substr(url.length - 4) != '.txt') { |
| var minVersion = testOptions.minVersion; |
| if (!minVersion) { |
| minVersion = hierarchicalOptions.defaultVersion; |
| } |
| var maxVersion = testOptions.maxVersion; |
| if (!maxVersion) { |
| maxVersion = hierarchicalOptions.defaultMaxVersion; |
| } |
| var slow = testOptions.slow; |
| if (!slow) { |
| slow = hierarchicalOptions.defaultSlow; |
| } |
| |
| if (globalOptions.fast && slow) { |
| useTest = false; |
| } else if (globalOptions.minVersion) { |
| useTest = greaterThanOrEqualToVersion(minVersion, globalOptions.minVersion); |
| } else if (globalOptions.maxVersion && maxVersion) { |
| useTest = greaterThanOrEqualToVersion(globalOptions.maxVersion, maxVersion); |
| } else { |
| useTest = greaterThanOrEqualToVersion(globalOptions.version, minVersion); |
| if (maxVersion) { |
| useTest = useTest && greaterThanOrEqualToVersion(maxVersion, globalOptions.version); |
| } |
| } |
| } |
| |
| if (!useTest) { |
| callback(true, []); |
| return; |
| } |
| |
| if (url.substr(url.length - 4) == '.txt') { |
| // If a version was explicity specified pass it down. |
| if (testOptions.minVersion) { |
| hierarchicalOptions.defaultVersion = testOptions.minVersion; |
| } |
| if (testOptions.maxVersion) { |
| hierarchicalOptions.defaultMaxVersion = testOptions.maxVersion; |
| } |
| if (testOptions.slow) { |
| hierarchicalOptions.defaultSlow = testOptions.slow; |
| } |
| loadTextFileAsynchronous(url, function() { |
| return function(success, text) { |
| if (!success) { |
| callback(false, ''); |
| return; |
| } |
| var lines = text.split('\n'); |
| var prefix = ''; |
| var lastSlash = url.lastIndexOf('/'); |
| if (lastSlash >= 0) { |
| prefix = url.substr(0, lastSlash + 1); |
| } |
| var fail = false; |
| var count = 1; |
| var index = 0; |
| for (var ii = 0; ii < lines.length; ++ii) { |
| var str = lines[ii].replace(/^\s\s*/, '').replace(/\s\s*$/, ''); |
| if (str.length > 4 && |
| str[0] != '#' && |
| str[0] != ";" && |
| str.substr(0, 2) != "//") { |
| ++count; |
| getFileListImpl(prefix, str, ii + 1, copyObject(hierarchicalOptions), function(index) { |
| return function(success, new_files) { |
| //log("got files: " + new_files.length); |
| if (success) { |
| files[index] = new_files; |
| } |
| finish(success); |
| }; |
| }(index++)); |
| } |
| } |
| finish(true); |
| |
| function finish(success) { |
| if (!success) { |
| fail = true; |
| } |
| --count; |
| //log("count: " + count); |
| if (!count) { |
| callback(!fail, files); |
| } |
| } |
| } |
| }()); |
| } else { |
| files.push(url); |
| callback(true, files); |
| } |
| }; |
| |
| getFileListImpl('', url, 1, globalOptions, function(success, files) { |
| // flatten |
| var flat = []; |
| flatten(files); |
| function flatten(files) { |
| for (var ii = 0; ii < files.length; ++ii) { |
| var value = files[ii]; |
| if (typeof(value) == "string") { |
| flat.push(value); |
| } else { |
| flatten(value); |
| } |
| } |
| } |
| callback(success, flat); |
| }); |
| }; |
| |
| var FilterURL = (function() { |
| var prefix = window.location.pathname; |
| prefix = prefix.substring(0, prefix.lastIndexOf("/") + 1); |
| return function(url) { |
| if (url.substring(0, prefix.length) == prefix) { |
| url = url.substring(prefix.length); |
| } |
| return url; |
| }; |
| }()); |
| |
| var TestFile = function(url) { |
| this.url = url; |
| }; |
| |
| var Test = function(file) { |
| this.file = file; |
| }; |
| |
| var TestHarness = function(iframe, filelistUrl, reportFunc, options) { |
| this.window = window; |
| this.iframes = iframe.length ? iframe : [iframe]; |
| this.reportFunc = reportFunc; |
| this.timeoutDelay = 20000; |
| this.files = []; |
| this.allowSkip = options.allowSkip; |
| this.webglVersion = getMajorVersion(options.version); |
| this.dumpShaders = options.dumpShaders; |
| this.quiet = options.quiet; |
| |
| var that = this; |
| getFileList(filelistUrl, function() { |
| return function(success, files) { |
| that.addFiles_(success, files); |
| }; |
| }(), options); |
| |
| }; |
| |
| TestHarness.reportType = { |
| ADD_PAGE: 1, |
| READY: 2, |
| START_PAGE: 3, |
| TEST_RESULT: 4, |
| FINISH_PAGE: 5, |
| FINISHED_ALL_TESTS: 6 |
| }; |
| |
| TestHarness.prototype.addFiles_ = function(success, files) { |
| if (!success) { |
| this.reportFunc( |
| TestHarness.reportType.FINISHED_ALL_TESTS, |
| '', |
| 'Unable to load tests. Are you running locally?\n' + |
| 'You need to run from a server or configure your\n' + |
| 'browser to allow access to local files (not recommended).\n\n' + |
| 'Note: An easy way to run from a server:\n\n' + |
| '\tcd path_to_tests\n' + |
| '\tpython -m SimpleHTTPServer\n\n' + |
| 'then point your browser to ' + |
| '<a href="http://localhost:8000/webgl-conformance-tests.html">' + |
| 'http://localhost:8000/webgl-conformance-tests.html</a>', |
| false) |
| return; |
| } |
| log("total files: " + files.length); |
| for (var ii = 0; ii < files.length; ++ii) { |
| log("" + ii + ": " + files[ii]); |
| this.files.push(new TestFile(files[ii])); |
| this.reportFunc(TestHarness.reportType.ADD_PAGE, '', files[ii], undefined); |
| } |
| this.reportFunc(TestHarness.reportType.READY, '', undefined, undefined); |
| } |
| |
| TestHarness.prototype.runTests = function(opt_options) { |
| var options = opt_options || { }; |
| options.start = options.start || 0; |
| options.count = options.count || this.files.length; |
| |
| this.idleIFrames = this.iframes.slice(0); |
| this.runningTests = {}; |
| var testsToRun = []; |
| for (var ii = 0; ii < options.count; ++ii) { |
| testsToRun.push(ii + options.start); |
| } |
| this.numTestsRemaining = options.count; |
| this.testsToRun = testsToRun; |
| this.startNextTest(); |
| }; |
| |
| TestHarness.prototype.setTimeout = function(test) { |
| var that = this; |
| test.timeoutId = this.window.setTimeout(function() { |
| that.timeout(test); |
| }, this.timeoutDelay); |
| }; |
| |
| TestHarness.prototype.clearTimeout = function(test) { |
| this.window.clearTimeout(test.timeoutId); |
| }; |
| |
| TestHarness.prototype.startNextTest = function() { |
| if (this.numTestsRemaining == 0) { |
| log("done"); |
| this.reportFunc(TestHarness.reportType.FINISHED_ALL_TESTS, |
| '', '', true); |
| } else { |
| while (this.testsToRun.length > 0 && this.idleIFrames.length > 0) { |
| var testId = this.testsToRun.shift(); |
| var iframe = this.idleIFrames.shift(); |
| this.startTest(iframe, this.files[testId], this.webglVersion); |
| } |
| } |
| }; |
| |
| TestHarness.prototype.startTest = function(iframe, testFile, webglVersion) { |
| var test = { |
| iframe: iframe, |
| testFile: testFile |
| }; |
| var url = testFile.url; |
| this.runningTests[url] = test; |
| log("loading: " + url); |
| if (this.reportFunc(TestHarness.reportType.START_PAGE, url, url, undefined)) { |
| iframe.src = getURLWithOptions(url, { |
| "webglVersion": webglVersion, |
| "dumpShaders": this.dumpShaders, |
| "quiet": this.quiet |
| }); |
| this.setTimeout(test); |
| } else { |
| this.reportResults(url, !!this.allowSkip, "skipped", true); |
| this.notifyFinished(url); |
| } |
| }; |
| |
| TestHarness.prototype.getTest = function(url) { |
| var test = this.runningTests[FilterURL(url)]; |
| if (!test) { |
| throw("unknown test:" + url); |
| } |
| return test; |
| }; |
| |
| TestHarness.prototype.reportResults = function(url, success, msg, skipped) { |
| url = FilterURL(url); |
| var test = this.getTest(url); |
| this.clearTimeout(test); |
| log((success ? "PASS" : "FAIL") + ": " + msg); |
| this.reportFunc(TestHarness.reportType.TEST_RESULT, url, msg, success, skipped); |
| // For each result we get, reset the timeout |
| this.setTimeout(test); |
| }; |
| |
| TestHarness.prototype.dequeTest = function(test) { |
| this.clearTimeout(test); |
| this.idleIFrames.push(test.iframe); |
| delete this.runningTests[test.testFile.url]; |
| --this.numTestsRemaining; |
| } |
| |
| TestHarness.prototype.notifyFinished = function(url) { |
| url = FilterURL(url); |
| var test = this.getTest(url); |
| log(url + ": finished"); |
| this.dequeTest(test); |
| this.reportFunc(TestHarness.reportType.FINISH_PAGE, url, url, true); |
| this.startNextTest(); |
| }; |
| |
| TestHarness.prototype.timeout = function(test) { |
| this.dequeTest(test); |
| var url = test.testFile.url; |
| log(url + ": timeout"); |
| this.reportFunc(TestHarness.reportType.FINISH_PAGE, url, url, undefined); |
| this.startNextTest(); |
| }; |
| |
| TestHarness.prototype.setTimeoutDelay = function(x) { |
| this.timeoutDelay = x; |
| }; |
| |
| return { |
| 'TestHarness': TestHarness, |
| 'getMajorVersion': getMajorVersion, |
| 'getURLWithOptions': getURLWithOptions |
| }; |
| |
| }(); |
| |
| |
| |