1<!DOCTYPE html>
2<!--
3Copyright (c) 2014 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/event.html">
9<link rel="import" href="/tracing/base/event_target.html">
10<link rel="import" href="/tracing/base/iteration_helpers.html">
11<link rel="import" href="/tracing/base/unittest/test_suite.html">
12<link rel="import" href="/tracing/base/xhr.html">
13
14<script>
15'use strict';
16
17tr.exportTo('tr.b.unittest', function() {
18  function HTMLImportsModuleLoader() {
19  }
20  HTMLImportsModuleLoader.prototype = {
21    loadModule: function(testRelpath, moduleName) {
22      return new Promise(function(resolve, reject) {
23        var importEl = document.createElement('link');
24        importEl.moduleName = moduleName;
25        importEl.setAttribute('rel', 'import');
26        importEl.setAttribute('href', testRelpath);
27
28        importEl.addEventListener('load', function() {
29          resolve({testRelpath: testRelpath,
30                   moduleName: moduleName});
31        });
32        importEl.addEventListener('error', function(e) {
33          reject('Error loading &#60;link rel="import" href="' +
34                 testRelpath + '"');
35        });
36
37        tr.doc.head.appendChild(importEl);
38      });
39    },
40
41    getCurrentlyExecutingModuleName: function() {
42      if (!document.currentScript)
43        throw new Error('Cannot call testSuite except during load.');
44      var linkDoc = document.currentScript.ownerDocument;
45      var url = linkDoc.URL;
46      var name = this.guessModuleNameFromURL_(url);
47      return name;
48    },
49
50    guessModuleNameFromURL_: function(url) {
51      var m = /.+?:\/\/.+?(\/.+)/.exec(url);
52      if (!m)
53        throw new Error('Guessing module name failed');
54      var path = m[1];
55      if (path[0] != '/')
56        throw new Error('malformed path');
57      if (path.substring(path.length - 5) != '.html')
58        throw new Error('Cannot define testSuites outside html imports');
59      var parts = path.substring(1, path.length - 5).split('/');
60      return parts.join('.');
61    }
62  };
63
64  function HeadlessModuleLoader() {
65    this.currentlyExecutingModuleInfo_ = undefined;
66  }
67  HeadlessModuleLoader.prototype = {
68    loadModule: function(testRelpath, moduleName) {
69      return Promise.resolve().then(function() {
70        var moduleInfo = {
71            testRelpath: testRelpath,
72            moduleName: moduleName
73        };
74        if (this.currentlyExecutingModuleInfo_ !== undefined)
75          throw new Error('WAT');
76        this.currentlyExecutingModuleInfo_ = moduleInfo;
77
78        try {
79          loadHTML(testRelpath);
80        } catch (e) {
81          e.message = 'While loading ' + moduleName + ', ' + e.message;
82          e.stack = 'While loading ' + moduleName + ', ' + e.stack;
83          throw e;
84        } finally {
85          this.currentlyExecutingModuleInfo_ = undefined;
86        }
87
88        return moduleInfo;
89      }.bind(this));
90    },
91
92    getCurrentlyExecutingModuleName: function() {
93      if (this.currentlyExecutingModuleInfo_ === undefined)
94        throw new Error('No currently loading module');
95      return this.currentlyExecutingModuleInfo_.moduleName;
96    }
97  };
98
99
100  function SuiteLoader(suiteRelpathsToLoad) {
101    tr.b.EventTarget.call(this);
102
103    this.currentModuleLoader_ = undefined;
104    this.testSuites = [];
105
106    if (tr.isHeadless) {
107      this.currentModuleLoader_ = new HeadlessModuleLoader();
108    } else {
109      this.currentModuleLoader_ = new HTMLImportsModuleLoader();
110    }
111
112    this.allSuitesLoadedPromise = this.beginLoadingModules_(
113        suiteRelpathsToLoad);
114  }
115
116  SuiteLoader.prototype = {
117    __proto__: tr.b.EventTarget.prototype,
118
119    beginLoadingModules_: function(testRelpaths) {
120      // Hooks!
121      this.bindGlobalHooks_();
122
123      // Load the modules.
124      var modulePromises = [];
125      for (var i = 0; i < testRelpaths.length; i++) {
126        var testRelpath = testRelpaths[i];
127        var moduleName = testRelpath.split('/').slice(-1)[0];
128
129        var p = this.currentModuleLoader_.loadModule(testRelpath, moduleName);
130        modulePromises.push(p);
131      }
132
133      var allModulesLoadedPromise = new Promise(function(resolve, reject) {
134        var remaining = modulePromises.length;
135        var resolved = false;
136        function oneMoreLoaded() {
137          if (resolved)
138            return;
139          remaining--;
140          if (remaining > 0)
141            return;
142          resolved = true;
143          resolve();
144        }
145
146        function oneRejected(e) {
147          if (resolved)
148            return;
149          resolved = true;
150          reject(e);
151        }
152
153        modulePromises.forEach(function(modulePromise) {
154          modulePromise.then(oneMoreLoaded, oneRejected);
155        });
156      });
157
158      // Script errors errors abort load;
159      var scriptErrorPromise = new Promise(function(xresolve, xreject) {
160        this.scriptErrorPromiseResolver_ = {
161          resolve: xresolve,
162          reject: xreject
163        };
164      }.bind(this));
165      var donePromise = Promise.race([
166        allModulesLoadedPromise,
167        scriptErrorPromise
168      ]);
169
170      // Cleanup.
171      return donePromise.then(
172        function() {
173          this.scriptErrorPromiseResolver_ = undefined;
174          this.unbindGlobalHooks_();
175        }.bind(this),
176        function(e) {
177          this.scriptErrorPromiseResolver_ = undefined;
178          this.unbindGlobalHooks_();
179          throw e;
180        }.bind(this));
181    },
182
183    bindGlobalHooks_: function() {
184      if (global._currentSuiteLoader !== undefined)
185        throw new Error('A suite loader exists already');
186      global._currentSuiteLoader = this;
187
188      this.oldGlobalOnError_ = global.onerror;
189      global.onerror = function(errorMsg, url, lineNumber) {
190        this.scriptErrorPromiseResolver_.reject(
191            new Error(errorMsg + '\n' + url + ':' + lineNumber));
192        if (this.oldGlobalOnError_)
193          return this.oldGlobalOnError_(errorMsg, url, lineNumber);
194        return false;
195      }.bind(this);
196    },
197
198    unbindGlobalHooks_: function() {
199      global._currentSuiteLoader = undefined;
200
201      global.onerror = this.oldGlobalOnError_;
202      this.oldGlobalOnError_ = undefined;
203    },
204
205    constructAndRegisterTestSuite: function(suiteConstructor) {
206      var name = this.currentModuleLoader_.getCurrentlyExecutingModuleName();
207
208      var testSuite = new tr.b.unittest.TestSuite(
209        name, suiteConstructor);
210
211      this.testSuites.push(testSuite);
212
213      var e = new tr.b.Event('suite-loaded');
214      e.testSuite = testSuite;
215      this.dispatchEvent(e);
216    },
217
218    getAllTests: function() {
219      var tests = [];
220      this.testSuites.forEach(function(suite) {
221        tests.push.apply(tests, suite.tests);
222      });
223      return tests;
224    },
225
226    findTestWithFullyQualifiedName: function(fullyQualifiedName) {
227      for (var i = 0; i < this.testSuites.length; i++) {
228        var suite = this.testSuites[i];
229        for (var j = 0; j < suite.tests.length; j++) {
230          var test = suite.tests[j];
231          if (test.fullyQualifiedName == fullyQualifiedName)
232            return test;
233        }
234      }
235      throw new Error('Test ' + fullyQualifiedName +
236                      'not found amongst ' + this.testSuites.length);
237    }
238  };
239
240  return {
241    SuiteLoader: SuiteLoader
242  };
243});
244</script>
245