Home | History | Annotate | Download | only in base
      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 (a] 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.
     18 if (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  */
     29 var 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  */
     36 var 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  */
     43 var jsDirBase = jsFileBase.replace(jsFile, '');
     44 
     45 /**
     46  * Path to Closure library style deps.js file.
     47  * @type {string?}
     48  */
     49 var depsFile = arguments[3];
     50 
     51 /**
     52  * Path to C++ file generation is outputting to.
     53  * @type {string}
     54  */
     55 var outputFile = arguments[4];
     56 
     57 /**
     58  * Type of this test.
     59  * @type {string} ('extension' | 'unit' | 'webui')
     60  */
     61 var testType = arguments[5];
     62 if (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  */
     73 var testF;
     74 
     75 /**
     76  * Keeps track of whether a typedef has been generated for each test
     77  * fixture.
     78  * @type {Object.<string, string>}
     79  */
     80 var 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  */
     87 var 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  */
     95 var addSetPreloadInfo;
     96 
     97 /**
     98  * Whether cc headers need to be generated.
     99  * @type {boolean}
    100  */
    101 var needGenHeader = true;
    102 
    103 /**
    104  * Helpful hint pointing back to the source js.
    105  * @type {string}
    106  */
    107 var 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  */
    114 function 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  */
    163 function 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  */
    186 var 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  */
    193 var dependencyPathsToRequires = {};
    194 
    195 if (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  */
    225 function 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  */
    278 function 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  */
    290 function 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  */
    308 function 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  */
    324 function 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|.
    376 var js = read(jsFile);
    377 eval(js);
    378