• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Generator script for creating gtest-style JavaScript
7 *     tests for extensions, WebUI and unit tests. Generates C++ gtest wrappers
8 *     which will invoke the appropriate JavaScript for each test.
9 * @author scr@chromium.org (Sheridan Rawlins)
10 * @see WebUI testing: http://goo.gl/ZWFXF
11 * @see gtest documentation: http://goo.gl/Ujj3H
12 * @see chrome/chrome_tests.gypi
13 * @see tools/gypv8sh.py
14 */
15
16// Arguments from rules in chrome_tests.gypi are passed in through
17// python script gypv8sh.py.
18if (arguments.length != 6) {
19  print('usage: ' +
20        arguments[0] +
21        ' path-to-testfile.js testfile.js path_to_deps.js output.cc test-type');
22  quit(-1);
23}
24
25/**
26 * Full path to the test input file.
27 * @type {string}
28 */
29var jsFile = arguments[1];
30
31/**
32 * Relative path to the test input file appropriate for use in the
33 * C++ TestFixture's addLibrary method.
34 * @type {string}
35 */
36var jsFileBase = arguments[2];
37
38/**
39 * The cwd, as determined by the paths of |jsFile| and |jsFileBase|.
40 * This is usually relative to the root source directory and points to the
41 * directory where the GYP rule processing the js file lives.
42 */
43var jsDirBase = jsFileBase.replace(jsFile, '');
44
45/**
46 * Path to Closure library style deps.js file.
47 * @type {string?}
48 */
49var depsFile = arguments[3];
50
51/**
52 * Path to C++ file generation is outputting to.
53 * @type {string}
54 */
55var outputFile = arguments[4];
56
57/**
58 * Type of this test.
59 * @type {string} ('extension' | 'unit' | 'webui')
60 */
61var testType = arguments[5];
62if (testType != 'extension' &&
63    testType != 'unit' &&
64    testType != 'webui') {
65  print('Invalid test type: ' + testType);
66  quit(-1);
67}
68
69/**
70 * C++ gtest macro to use for TEST_F depending on |testType|.
71 * @type {string} ('TEST_F'|'IN_PROC_BROWSER_TEST_F')
72 */
73var testF;
74
75/**
76 * Keeps track of whether a typedef has been generated for each test
77 * fixture.
78 * @type {Object.<string, string>}
79 */
80var typedeffedCppFixtures = {};
81
82/**
83 * Maintains a list of relative file paths to add to each gtest body
84 * for inclusion at runtime before running each JavaScript test.
85 * @type {Array.<string>}
86 */
87var genIncludes = [];
88
89/**
90 * When true, add calls to set_preload_test_(fixture|name). This is needed when
91 * |testType| === 'webui' to send an injection message before the page loads,
92 * but is not required or supported by any other test type.
93 * @type {boolean}
94 */
95var addSetPreloadInfo;
96
97/**
98 * Whether cc headers need to be generated.
99 * @type {boolean}
100 */
101var needGenHeader = true;
102
103/**
104 * Helpful hint pointing back to the source js.
105 * @type {string}
106 */
107var argHint = '// ' + this['arguments'].join(' ');
108
109
110/**
111 * Generates the header of the cc file to stdout.
112 * @param {string?} testFixture Name of test fixture.
113 */
114function maybeGenHeader(testFixture) {
115  if (!needGenHeader)
116    return;
117  needGenHeader = false;
118  print('// GENERATED FILE');
119  print(argHint);
120  print('// PLEASE DO NOT HAND EDIT!');
121  print();
122
123  // Output some C++ headers based upon the |testType|.
124  //
125  // Currently supports:
126  // 'extension' - browser_tests harness, js2extension rule,
127  //               ExtensionJSBrowserTest superclass.
128  // 'unit' - unit_tests harness, js2unit rule, V8UnitTest superclass.
129  // 'webui' - browser_tests harness, js2webui rule, WebUIBrowserTest
130  // superclass.
131  if (testType === 'extension') {
132    print('#include "chrome/test/base/extension_js_browser_test.h"');
133    testing.Test.prototype.typedefCppFixture = 'ExtensionJSBrowserTest';
134    addSetPreloadInfo = false;
135    testF = 'IN_PROC_BROWSER_TEST_F';
136  } else if (testType === 'unit') {
137    print('#include "chrome/test/base/v8_unit_test.h"');
138    testing.Test.prototype.typedefCppFixture = 'V8UnitTest';
139    testF = 'TEST_F';
140    addSetPreloadInfo = false;
141  } else {
142    print('#include "chrome/test/base/web_ui_browser_test.h"');
143    testing.Test.prototype.typedefCppFixture = 'WebUIBrowserTest';
144    testF = 'IN_PROC_BROWSER_TEST_F';
145    addSetPreloadInfo = true;
146  }
147  print('#include "url/gurl.h"');
148  print('#include "testing/gtest/include/gtest/gtest.h"');
149  if (testFixture && this[testFixture].prototype.testGenCppIncludes)
150    this[testFixture].prototype.testGenCppIncludes();
151  print();
152}
153
154
155/**
156 * Convert the |includeFile| to paths appropriate for immediate
157 * inclusion (path) and runtime inclusion (base).
158 * @param {string} includeFile The file to include.
159 * @return {{path: string, base: string}} Object describing the paths
160 *     for |includeFile|. |path| is relative to cwd; |base| is relative to
161 * source root.
162 */
163function includeFileToPaths(includeFile) {
164  if (includeFile.indexOf(jsDirBase) == 0) {
165    // The caller supplied a path relative to root source.
166    var relPath = includeFile.replace(jsDirBase, '');
167    return {
168      path: relPath,
169      base: jsDirBase + relPath
170    };
171  }
172
173  // The caller supplied a path relative to the input js file's directory (cwd).
174  return {
175    path: jsFile.replace(/[^\/\\]+$/, includeFile),
176    base: jsFileBase.replace(/[^\/\\]+$/, includeFile),
177  };
178}
179
180
181/**
182 * Maps object names to the path to the file that provides them.
183 * Populated from the |depsFile| if any.
184 * @type {Object.<string, string>}
185 */
186var dependencyProvidesToPaths = {};
187
188/**
189 * Maps dependency path names to object names required by the file.
190 * Populated from the |depsFile| if any.
191 * @type {Object.<string, Array.<string>>}
192 */
193var dependencyPathsToRequires = {};
194
195if (depsFile) {
196  var goog = goog || {};
197  /**
198   * Called by the javascript in the deps file to add modules and their
199   * dependencies.
200   * @param {string} path Relative path to the file.
201   * @param Array.<string> provides Objects provided by this file.
202   * @param Array.<string> requires Objects required by this file.
203   */
204  goog.addDependency = function(path, provides, requires) {
205    provides.forEach(function(provide) {
206      dependencyProvidesToPaths[provide] = path;
207    });
208    dependencyPathsToRequires[path] = requires;
209  };
210
211  // Read and eval the deps file.  It should only contain goog.addDependency
212  // calls.
213  eval(read(depsFile));
214}
215
216/**
217 * Resolves a list of libraries to an ordered list of paths to load by the
218 * generated C++.  The input should contain object names provided
219 * by the deps file.  Dependencies will be resolved and included in the
220 * correct order, meaning that the returned array may contain more entries
221 * than the input.
222 * @param {Array.<string>} deps List of dependencies.
223 * @return {Array.<string>} List of paths to load.
224 */
225function resolveClosureModuleDeps(deps) {
226  if (!depsFile && deps.length > 0) {
227    print('Can\'t have closure dependencies without a deps file.');
228    quit(-1);
229  }
230  var resultPaths = [];
231  var addedPaths = {};
232
233  function addPath(path) {
234    addedPaths[path] = true;
235    resultPaths.push(path);
236  }
237
238  function resolveAndAppend(path) {
239    if (addedPaths[path]) {
240      return;
241    }
242    // Set before recursing to catch cycles.
243    addedPaths[path] = true;
244    dependencyPathsToRequires[path].forEach(function(require) {
245      var providingPath = dependencyProvidesToPaths[require];
246      if (!providingPath) {
247        print('Unknown object', require, 'required by', path);
248        quit(-1);
249      }
250      resolveAndAppend(providingPath);
251    });
252    resultPaths.push(path);
253  }
254
255  // Always add closure library's base.js if provided by deps.
256  var basePath = dependencyProvidesToPaths['goog'];
257  if (basePath) {
258    addPath(basePath);
259  }
260
261  deps.forEach(function(dep) {
262    var providingPath = dependencyProvidesToPaths[dep];
263    if (providingPath) {
264      resolveAndAppend(providingPath);
265    } else {
266      print('Unknown dependency:', dep);
267      quit(-1);
268    }
269  });
270
271  return resultPaths;
272}
273
274/**
275 * Output |code| verbatim.
276 * @param {string} code The code to output.
277 */
278function GEN(code) {
279  maybeGenHeader(null);
280  print(code);
281}
282
283/**
284 * Outputs |commentEncodedCode|, converting comment to enclosed C++ code.
285 * @param {function} commentEncodedCode A function in the following format (note
286 * the space in '/ *' and '* /' should be removed to form a comment delimiter):
287 *    function() {/ *! my_cpp_code.DoSomething(); * /
288 *    Code between / *! and * / will be extracted and written to stdout.
289 */
290function GEN_BLOCK(commentEncodedCode) {
291  var code = commentEncodedCode.toString().
292      replace(/^[^\/]+\/\*!?/, '').
293      replace(/\*\/[^\/]+$/, '').
294      replace(/^\n|\n$/, '').
295      replace(/\s+$/, '');
296  GEN(code);
297}
298
299/**
300 * Generate includes for the current |jsFile| by including them
301 * immediately and at runtime.
302 * The paths are allowed to be:
303 *   1. relative to the root src directory (i.e. similar to #include's).
304 *   2. relative to the directory specified in the GYP rule for the file.
305 * @param {Array.<string>} includes Paths to JavaScript files to
306 *     include immediately and at runtime.
307 */
308function GEN_INCLUDE(includes) {
309  for (var i = 0; i < includes.length; i++) {
310    var includePaths = includeFileToPaths(includes[i]);
311    var js = read(includePaths.path);
312    ('global', eval)(js);
313    genIncludes.push(includePaths.base);
314  }
315}
316
317/**
318 * Generate gtest-style TEST_F definitions for C++ with a body that
319 * will invoke the |testBody| for |testFixture|.|testFunction|.
320 * @param {string} testFixture The name of this test's fixture.
321 * @param {string} testFunction The name of this test's function.
322 * @param {Function} testBody The function body to execute for this test.
323 */
324function TEST_F(testFixture, testFunction, testBody) {
325  maybeGenHeader(testFixture);
326  var browsePreload = this[testFixture].prototype.browsePreload;
327  var browsePrintPreload = this[testFixture].prototype.browsePrintPreload;
328  var testGenPreamble = this[testFixture].prototype.testGenPreamble;
329  var testGenPostamble = this[testFixture].prototype.testGenPostamble;
330  var typedefCppFixture = this[testFixture].prototype.typedefCppFixture;
331  var isAsyncParam = testType === 'unit' ? '' :
332      this[testFixture].prototype.isAsync + ', ';
333  var testShouldFail = this[testFixture].prototype.testShouldFail;
334  var testPredicate = testShouldFail ? 'ASSERT_FALSE' : 'ASSERT_TRUE';
335  var extraLibraries = genIncludes.concat(
336      this[testFixture].prototype.extraLibraries.map(
337          function(includeFile) {
338            return includeFileToPaths(includeFile).base;
339          }),
340      resolveClosureModuleDeps(this[testFixture].prototype.closureModuleDeps));
341
342  if (typedefCppFixture && !(testFixture in typedeffedCppFixtures)) {
343    print('typedef ' + typedefCppFixture + ' ' + testFixture + ';');
344    typedeffedCppFixtures[testFixture] = typedefCppFixture;
345  }
346
347  print(testF + '(' + testFixture + ', ' + testFunction + ') {');
348  for (var i = 0; i < extraLibraries.length; i++) {
349    print('  AddLibrary(base::FilePath(FILE_PATH_LITERAL("' +
350        extraLibraries[i].replace(/\\/g, '/') + '")));');
351  }
352  print('  AddLibrary(base::FilePath(FILE_PATH_LITERAL("' +
353      jsFileBase.replace(/\\/g, '/') + '")));');
354  if (addSetPreloadInfo) {
355    print('  set_preload_test_fixture("' + testFixture + '");');
356    print('  set_preload_test_name("' + testFunction + '");');
357  }
358  if (testGenPreamble)
359    testGenPreamble(testFixture, testFunction);
360  if (browsePreload)
361    print('  BrowsePreload(GURL("' + browsePreload + '"));');
362  if (browsePrintPreload) {
363    print('  BrowsePrintPreload(GURL(WebUITestDataPathToURL(\n' +
364          '      FILE_PATH_LITERAL("' + browsePrintPreload + '"))));');
365  }
366  print('  ' + testPredicate + '(RunJavascriptTestF(' + isAsyncParam +
367        '"' + testFixture + '", ' +
368        '"' + testFunction + '"));');
369  if (testGenPostamble)
370    testGenPostamble(testFixture, testFunction);
371  print('}');
372  print();
373}
374
375// Now that generation functions are defined, load in |jsFile|.
376var js = read(jsFile);
377eval(js);
378