tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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