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