diff options
author | scr@chromium.org <scr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-05 04:24:20 +0000 |
---|---|---|
committer | scr@chromium.org <scr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-05 04:24:20 +0000 |
commit | 26d47586aab3b841a1ecbe7bdfcabb86f5243ff2 (patch) | |
tree | 7b90a780e71bcdd8ea5bcd4671952972e4424a0d | |
parent | d15c370b5043f7959e05fe6be68599093d1554fd (diff) | |
download | chromium_src-26d47586aab3b841a1ecbe7bdfcabb86f5243ff2.zip chromium_src-26d47586aab3b841a1ecbe7bdfcabb86f5243ff2.tar.gz chromium_src-26d47586aab3b841a1ecbe7bdfcabb86f5243ff2.tar.bz2 |
Allow javascript unit tests using webui test_api framework.
I moved javascript2webui.js over to chrome/test/base/js2gtest.js and shared it
across webui and unit tests with an extra parameter for test type, which governs
the few differences, such as what to include/subclass from and the flavor of
gtest (TEST_F v. IN_PROC_BROWSER_TEST_F).
v8_unit_test implemented 2 main methods to make it work with the generated C++
- AddLibrary - loads the file in the test context
- RunJavascriptF - launches the runTest with the testFixture and testName,
coordinating with Error and ChromeSend to report results.
Helper functions:
- Error - watches for console.error, noting failure if seen.
- ChromeSend - pulls apart the result from the test_api's call to
chrome.send('testResult', [ok, msg])
R=arv@chromium.org
BUG=87820
TEST=unit_tests --gtest_filter=FrameworkUnitTest.*
Committed: http://src.chromium.org/viewvc/chrome?view=rev&revision=108391
Review URL: http://codereview.chromium.org/8418015
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@108773 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/chrome_tests.gypi | 53 | ||||
-rw-r--r-- | chrome/test/base/js2gtest.js (renamed from chrome/browser/ui/webui/javascript2webui.js) | 25 | ||||
-rw-r--r-- | chrome/test/base/v8_unit_test.cc | 200 | ||||
-rw-r--r-- | chrome/test/base/v8_unit_test.h | 56 | ||||
-rw-r--r-- | chrome/test/data/unit/framework_unittest.js | 42 | ||||
-rw-r--r-- | tools/gypv8sh.py | 12 |
6 files changed, 340 insertions, 48 deletions
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 46993a3..59cc637 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -3,6 +3,13 @@ # found in the LICENSE file. { 'variables' : { + # Variables for js2gtest rules + 'gypv8sh': '../tools/gypv8sh.py', + 'js2gtest': 'test/base/js2gtest.js', + 'js2gtest_out_dir': '<(SHARED_INTERMEDIATE_DIR)/js2gtest', + 'mock_js': 'third_party/mock4js/mock4js.js', + 'test_api_js': 'test/data/webui/test_api.js', + 'pyautolib_sources': [ 'app/chrome_command_ids.h', 'app/chrome_dll_resource.h', @@ -1945,6 +1952,7 @@ 'test/base/v8_unit_test.cc', 'test/base/v8_unit_test.h', 'test/data/resource.rc', + 'test/data/unit/framework_unittest.js', 'tools/convert_dict/convert_dict_unittest.cc', '../content/browser/renderer_host/render_widget_host_unittest.cc', '../content/browser/renderer_host/text_input_client_mac_unittest.mm', @@ -1966,7 +1974,40 @@ '../webkit/quota/mock_storage_client.cc', '../webkit/quota/mock_storage_client.h', ], + 'rules': [ + { + 'rule_name': 'js2unit', + 'extension': 'js', + 'msvs_external_rule': 1, + 'inputs': [ + '<(gypv8sh)', + '<(PRODUCT_DIR)/v8_shell<(EXECUTABLE_SUFFIX)', + '<(mock_js)', + '<(test_api_js)', + '<(js2gtest)', + ], + 'outputs': [ + '<(js2gtest_out_dir)/chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).cc', + '<(PRODUCT_DIR)/test_data/chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js', + ], + 'process_outputs_as_sources': 1, + 'action': [ + 'python', + '<@(_inputs)', + 'unit', + '<(RULE_INPUT_PATH)', + 'chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js', + '<@(_outputs)', + ], + }, + ], 'conditions': [ + ['target_arch!="arm"', { + 'dependencies': [ + # build time dependency. + '../v8/tools/gyp/v8.gyp:v8_shell#host', + ], + }], ['p2p_apis==1', { 'sources': [ '../content/browser/renderer_host/p2p/socket_host_test_utils.h', @@ -2284,13 +2325,6 @@ 'type': 'executable', 'msvs_cygwin_shell': 0, 'msvs_cygwin_dirs': ['<(DEPTH)/third_party/cygwin'], - 'variables': { - 'gypv8sh': '../tools/gypv8sh.py', - 'js2webui': 'browser/ui/webui/javascript2webui.js', - 'js2webui_out_dir': '<(SHARED_INTERMEDIATE_DIR)/js2webui', - 'mock_js': 'third_party/mock4js/mock4js.js', - 'test_api_js': 'test/data/webui/test_api.js', - }, 'dependencies': [ 'browser', 'browser/sync/protocol/sync_proto.gyp:sync_proto', @@ -2646,16 +2680,17 @@ '<(PRODUCT_DIR)/v8_shell<(EXECUTABLE_SUFFIX)', '<(mock_js)', '<(test_api_js)', - '<(js2webui)', + '<(js2gtest)', ], 'outputs': [ - '<(js2webui_out_dir)/chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).cc', + '<(js2gtest_out_dir)/chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).cc', '<(PRODUCT_DIR)/test_data/chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js', ], 'process_outputs_as_sources': 1, 'action': [ 'python', '<@(_inputs)', + 'webui', '<(RULE_INPUT_PATH)', 'chrome/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js', '<@(_outputs)', diff --git a/chrome/browser/ui/webui/javascript2webui.js b/chrome/test/base/js2gtest.js index e1e380f..995a257 100644 --- a/chrome/browser/ui/webui/javascript2webui.js +++ b/chrome/test/base/js2gtest.js @@ -1,21 +1,33 @@ // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -if (arguments.length < 3) { +if (arguments.length < 4) { print('usage: ' + - arguments[0] + ' path-to-testfile.js testfile.js [output.cc]'); + arguments[0] + ' path-to-testfile.js testfile.js output.cc test-type'); quit(-1); } var jsFile = arguments[1]; var jsFileBase = arguments[2]; var outputFile = arguments[3]; +var testType = arguments[4]; // Generate the file to stdout. print('// GENERATED FILE'); print('// ' + arguments.join(' ')); print('// PLEASE DO NOT HAND EDIT!'); print(); -print('#include "chrome/browser/ui/webui/web_ui_browsertest.h"'); + +var testF; + +if (testType === 'unit') { + print('#include "chrome/test/base/v8_unit_test.h"'); + testing.Test.prototype.typedefCppFixture = 'V8UnitTest'; + testF = 'TEST_F'; +} else { + print('#include "chrome/browser/ui/webui/web_ui_browsertest.h"'); + testing.Test.prototype.typedefCppFixture = 'WebUIBrowserTest'; + testF = 'IN_PROC_BROWSER_TEST_F'; +} print('#include "googleurl/src/gurl.h"'); print('#include "testing/gtest/include/gtest/gtest.h"'); print(); @@ -32,7 +44,8 @@ function TEST_F(testFixture, testFunction, testBody) { var testGenPreamble = this[testFixture].prototype.testGenPreamble; var testGenPostamble = this[testFixture].prototype.testGenPostamble; var typedefCppFixture = this[testFixture].prototype.typedefCppFixture; - var isAsync = this[testFixture].prototype.isAsync; + var isAsyncParam = testType === 'unit' ? '' : + this[testFixture].prototype.isAsync + ', '; var testShouldFail = this[testFixture].prototype.testShouldFail; var testPredicate = testShouldFail ? 'ASSERT_FALSE' : 'ASSERT_TRUE'; @@ -41,7 +54,7 @@ function TEST_F(testFixture, testFunction, testBody) { typedeffedCppFixtures[testFixture] = typedefCppFixture; } - print('IN_PROC_BROWSER_TEST_F(' + testFixture + ', ' + testFunction + ') {'); + print(testF + '(' + testFixture + ', ' + testFunction + ') {'); if (testGenPreamble) testGenPreamble(testFixture, testFunction); print(' AddLibrary(FilePath(FILE_PATH_LITERAL("' + @@ -55,7 +68,7 @@ function TEST_F(testFixture, testFunction, testBody) { ' FILE_PATH_LITERAL("' + browsePrintPreload + '"))),\n' + ' "' + testFixture + '", "' + testFunction + '");'); } - print(' ' + testPredicate + '(RunJavascriptTestF(' + isAsync + ', ' + + print(' ' + testPredicate + '(RunJavascriptTestF(' + isAsyncParam + '"' + testFixture + '", ' + '"' + testFunction + '"));'); if (testGenPostamble) diff --git a/chrome/test/base/v8_unit_test.cc b/chrome/test/base/v8_unit_test.cc index 798a03f..2456a73 100644 --- a/chrome/test/base/v8_unit_test.cc +++ b/chrome/test/base/v8_unit_test.cc @@ -4,17 +4,166 @@ #include "chrome/test/base/v8_unit_test.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" #include "base/string_piece.h" #include "base/stringprintf.h" +#include "chrome/common/chrome_paths.h" -V8UnitTest::V8UnitTest() {} +namespace { + +// |args| are passed through the various JavaScript logging functions such as +// console.log. Returns a string appropriate for logging with LOG(severity). +std::string LogArgs2String(const v8::Arguments& args) { + std::string message; + bool first = true; + for (int i = 0; i < args.Length(); i++) { + v8::HandleScope handle_scope; + if (first) + first = false; + else + message += " "; + + v8::String::Utf8Value str(args[i]); + message += *str; + } + return message; +} + +// Whether errors were seen. +bool had_errors = false; + +// testDone results. +bool testResult_ok = false; + +// Location of test data (currently test/data/webui). +FilePath test_data_directory; + +// Location of generated test data (<(PROGRAM_DIR)/test_data). +FilePath gen_test_data_directory; + +} // namespace + +V8UnitTest::V8UnitTest() { + InitPathsAndLibraries(); +} V8UnitTest::~V8UnitTest() {} +void V8UnitTest::AddLibrary(const FilePath& library_path) { + user_libraries_.push_back(library_path); +} + +bool V8UnitTest::ExecuteJavascriptLibraries() { + std::string utf8_content; + for (std::vector<FilePath>::iterator user_libraries_iterator = + user_libraries_.begin(); + user_libraries_iterator != user_libraries_.end(); + ++user_libraries_iterator) { + std::string library_content; + FilePath library_file(*user_libraries_iterator); + if (!user_libraries_iterator->IsAbsolute()) { + FilePath gen_file = gen_test_data_directory.Append(library_file); + library_file = file_util::PathExists(gen_file) ? gen_file : + test_data_directory.Append(*user_libraries_iterator); + } + if (!file_util::ReadFileToString(library_file, &library_content)) { + ADD_FAILURE() << library_file.value(); + return false; + } + ExecuteScriptInContext(library_content, library_file.MaybeAsASCII()); + if (::testing::Test::HasFatalFailure()) + return false; + } + return true; +} + +bool V8UnitTest::RunJavascriptTestF( + const std::string& testFixture, const std::string& testName) { + had_errors = false; + testResult_ok = false; + std::string test_js; + if (!ExecuteJavascriptLibraries()) + return false; + + v8::Context::Scope context_scope(context_); + v8::HandleScope handle_scope; + + v8::Handle<v8::Value> functionProperty = + context_->Global()->Get(v8::String::New("runTest")); + EXPECT_FALSE(functionProperty.IsEmpty()); + if (::testing::Test::HasNonfatalFailure()) + return false; + EXPECT_TRUE(functionProperty->IsFunction()); + if (::testing::Test::HasNonfatalFailure()) + return false; + v8::Handle<v8::Function> function = + v8::Handle<v8::Function>::Cast(functionProperty); + + v8::Local<v8::Array> params = v8::Array::New(); + params->Set(0, v8::String::New(testFixture.data(), testFixture.size())); + params->Set(1, v8::String::New(testName.data(), testName.size())); + v8::Handle<v8::Value> args[] = { + v8::Boolean::New(false), + v8::String::New("RUN_TEST_F"), + params + }; + + v8::TryCatch try_catch; + v8::Handle<v8::Value> result = function->Call(context_->Global(), 3, args); + // The test fails if an exception was thrown. + EXPECT_FALSE(result.IsEmpty()); + if (::testing::Test::HasNonfatalFailure()) + return false; + + // Ok if ran successfully, passed tests, and didn't have console errors. + return result->BooleanValue() && testResult_ok && !had_errors; +} + +void V8UnitTest::InitPathsAndLibraries() { + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_data_directory)); + test_data_directory = test_data_directory.AppendASCII("webui"); + ASSERT_TRUE(PathService::Get(chrome::DIR_GEN_TEST_DATA, + &gen_test_data_directory)); + + FilePath mockPath; + ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &mockPath)); + mockPath = mockPath.AppendASCII("chrome"); + mockPath = mockPath.AppendASCII("third_party"); + mockPath = mockPath.AppendASCII("mock4js"); + mockPath = mockPath.AppendASCII("mock4js.js"); + AddLibrary(mockPath); + + FilePath testApiPath; + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &testApiPath)); + testApiPath = testApiPath.AppendASCII("webui"); + testApiPath = testApiPath.AppendASCII("test_api.js"); + AddLibrary(testApiPath); +} + void V8UnitTest::SetUp() { v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New(); - global->Set(v8::String::New("log"), - v8::FunctionTemplate::New(&V8UnitTest::Log)); + v8::Handle<v8::String> logString = v8::String::New("log"); + v8::Handle<v8::FunctionTemplate> logFunction = + v8::FunctionTemplate::New(&V8UnitTest::Log); + global->Set(logString, logFunction); + + // Set up chrome object for chrome.send(). + v8::Handle<v8::ObjectTemplate> chrome = v8::ObjectTemplate::New(); + global->Set(v8::String::New("chrome"), chrome); + chrome->Set(v8::String::New("send"), + v8::FunctionTemplate::New(&V8UnitTest::ChromeSend)); + + // Set up console object for console.log(), etc. + v8::Handle<v8::ObjectTemplate> console = v8::ObjectTemplate::New(); + global->Set(v8::String::New("console"), console); + console->Set(logString, logFunction); + console->Set(v8::String::New("info"), logFunction); + console->Set(v8::String::New("warn"), logFunction); + console->Set(v8::String::New("error"), + v8::FunctionTemplate::New(&V8UnitTest::Error)); + context_ = v8::Context::New(NULL, global); } @@ -85,18 +234,39 @@ void V8UnitTest::TestFunction(const std::string& function_name) { // static v8::Handle<v8::Value> V8UnitTest::Log(const v8::Arguments& args) { - std::string message; - bool first = true; - for (int i = 0; i < args.Length(); i++) { - v8::HandleScope handle_scope; - if (first) { - first = false; - } else { - message += " "; - } - v8::String::Utf8Value str(args[i]); - message += *str; + LOG(INFO) << LogArgs2String(args); + return v8::Undefined(); +} + +v8::Handle<v8::Value> V8UnitTest::Error(const v8::Arguments& args) { + had_errors = true; + LOG(ERROR) << LogArgs2String(args); + return v8::Undefined(); +} + +v8::Handle<v8::Value> V8UnitTest::ChromeSend(const v8::Arguments& args) { + v8::HandleScope handle_scope; + // We expect to receive 2 args: ("testResult", [ok, message]). However, + // chrome.send may pass only one. Therefore we need to ensure we have at least + // 1, then ensure that the first is "testResult" before checking again for 2. + EXPECT_LE(1, args.Length()); + if (::testing::Test::HasNonfatalFailure()) + return v8::Undefined(); + v8::String::Utf8Value message(args[0]); + EXPECT_EQ("testResult", std::string(*message, message.length())); + if (::testing::Test::HasNonfatalFailure()) + return v8::Undefined(); + EXPECT_EQ(2, args.Length()); + if (::testing::Test::HasNonfatalFailure()) + return v8::Undefined(); + v8::Handle<v8::Array> testResult(args[1].As<v8::Array>()); + EXPECT_EQ(2U, testResult->Length()); + if (::testing::Test::HasNonfatalFailure()) + return v8::Undefined(); + testResult_ok = testResult->Get(0)->BooleanValue(); + if (!testResult_ok) { + v8::String::Utf8Value message(testResult->Get(1)); + LOG(ERROR) << *message; } - std::cout << message << "\n"; return v8::Undefined(); } diff --git a/chrome/test/base/v8_unit_test.h b/chrome/test/base/v8_unit_test.h index 243b332..9391dae 100644 --- a/chrome/test/base/v8_unit_test.h +++ b/chrome/test/base/v8_unit_test.h @@ -7,7 +7,9 @@ #pragma once #include <string> +#include <vector> +#include "base/file_path.h" #include "testing/gtest/include/gtest/gtest.h" #include "v8/include/v8.h" @@ -23,37 +25,67 @@ class V8UnitTest : public testing::Test { V8UnitTest(); virtual ~V8UnitTest(); - virtual void SetUp(); + // Methods from testing::Test. + virtual void SetUp() OVERRIDE; protected: - // Executes the given script source in the context. The specified script - // name is used when reporting errors. + // Add a custom helper JS library for your test. If |library_path| is + // relative, it'll be read as relative to the test data dir. + void AddLibrary(const FilePath& library_path); + + // Runs |test_fixture|.|test_name| using the framework in test_api.js. + bool RunJavascriptTestF(const std::string& test_fixture, + const std::string& test_name); + + // Executes the given |script_source| in the context. The specified + // |script_name| is used when reporting errors. virtual void ExecuteScriptInContext(const base::StringPiece& script_source, const base::StringPiece& script_name); - // Set a variable to a string value in the global scope. + // Set the variable |var_name| to a string |value| in the global scope. virtual void SetGlobalStringVar(const std::string& var_name, const std::string& value); - // Converts a v8::TryCatch into a human readable string. + // Converts the v8::TryCatch |try_catch| into a human readable string. virtual std::string ExceptionToString(const v8::TryCatch& try_catch); - // Calls the specified function that resides in the global scope of the - // context. If the function throws an exception, FAIL() is called to - // indicate a unit test failure. This is useful for executing unit test - // functions implemented in JavaScript. + // Calls the specified |function_name| that resides in the global scope of the + // context. If the function throws an exception, FAIL() is called to indicate + // a unit test failure. This is useful for executing unit test functions + // implemented in JavaScript. virtual void TestFunction(const std::string& function_name); - // This method is bound to a global function "log" in the context. - // Scripts running in the context can call this to print out logging - // information to the console. + // This method is bound to a global function "log" in the context, as well as + // to log, warn, and info of the console object. Scripts running in the + // context can call this with |args| to print out logging information to the + // console. static v8::Handle<v8::Value> Log(const v8::Arguments& args); + // This method is bound to console.error in the context. Any calls to this + // will log |args| to the console and also signal an error condition causing + // |RunJavascriptF| to fail. + static v8::Handle<v8::Value> Error(const v8::Arguments& args); + + // This method is bound to a method "chrome.send" in the context. When + // test_api calls testDone with |args| to report its results, this will + // capture and hold the results for analysis by |RunJavascriptF|. + static v8::Handle<v8::Value> ChromeSend(const v8::Arguments& args); + + private: + // Executes all added javascript libraries. Returns true if no errors. + bool ExecuteJavascriptLibraries(); + + // Initializes paths and libraries. + void InitPathsAndLibraries(); + // Handle scope that is used throughout the life of this class. v8::HandleScope handle_scope_; // Context for the JavaScript in the test. v8::Handle<v8::Context> context_; + + // User added libraries. + std::vector<FilePath> user_libraries_; }; #endif // CHROME_TEST_BASE_V8_UNIT_TEST_H_ diff --git a/chrome/test/data/unit/framework_unittest.js b/chrome/test/data/unit/framework_unittest.js new file mode 100644 index 0000000..3eee034 --- /dev/null +++ b/chrome/test/data/unit/framework_unittest.js @@ -0,0 +1,42 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Class for testing the unit_test framework. + * @constructor + */ +function FrameworkUnitTest() {} + +FrameworkUnitTest.prototype = { + __proto__: testing.Test.prototype, +}; + +TEST_F('FrameworkUnitTest', 'testExpectTrueOk', function() { + expectTrue(true); +}); + +TEST_F('FrameworkUnitTest', 'testAssertTrueOk', function() { + assertTrue(true); +}); + +/** + * Failing version of FrameworkUnitTest. + * @constructor + */ +function FrameworkUnitTestFail() {} + +FrameworkUnitTestFail.prototype = { + __proto__: FrameworkUnitTest.prototype, + + /** inheritDoc */ + testShouldFail: true, +}; + +TEST_F('FrameworkUnitTestFail', 'testExpectFailFails', function() { + expectNotReached(); +}); + +TEST_F('FrameworkUnitTestFail', 'testAssertFailFails', function() { + assertNotReached(); +});
\ No newline at end of file diff --git a/tools/gypv8sh.py b/tools/gypv8sh.py index ca9c759..7018db9 100644 --- a/tools/gypv8sh.py +++ b/tools/gypv8sh.py @@ -24,18 +24,18 @@ import shutil def main (): parser = optparse.OptionParser() parser.set_usage( - "%prog v8_shell mock.js test_api.js js2webui.js inputfile inputrelfile " - "cxxoutfile jsoutfile") + "%prog v8_shell mock.js test_api.js js2webui.js " + "testtype inputfile inputrelfile cxxoutfile jsoutfile") parser.add_option('-v', '--verbose', action='store_true') parser.add_option('-n', '--impotent', action='store_true', help="don't execute; just print (as if verbose)") (opts, args) = parser.parse_args() - if len(args) != 8: + if len(args) != 9: parser.error('all arguments are required.') - (v8_shell, mock_js, test_api, js2webui, inputfile, inputrelfile, - cxxoutfile, jsoutfile) = args - arguments = [js2webui, inputfile, inputrelfile, cxxoutfile] + (v8_shell, mock_js, test_api, js2webui, test_type, + inputfile, inputrelfile, cxxoutfile, jsoutfile) = args + arguments = [js2webui, inputfile, inputrelfile, cxxoutfile, test_type] cmd = [v8_shell, '-e', "arguments=" + json.dumps(arguments), mock_js, test_api, js2webui] if opts.verbose or opts.impotent: |