summaryrefslogtreecommitdiffstats
path: root/gin
diff options
context:
space:
mode:
authorabarth@chromium.org <abarth@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-11-25 21:26:15 +0000
committerabarth@chromium.org <abarth@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-11-25 21:26:15 +0000
commit2f70342531d0cb7335ea88ec5579d0a8a8bb114f (patch)
treebf1883a26ccf8493b5419fb239190619c249481b /gin
parentae6f06153630c4c0940a4630443ae4faf44ef409 (diff)
downloadchromium_src-2f70342531d0cb7335ea88ec5579d0a8a8bb114f.zip
chromium_src-2f70342531d0cb7335ea88ec5579d0a8a8bb114f.tar.gz
chromium_src-2f70342531d0cb7335ea88ec5579d0a8a8bb114f.tar.bz2
[Gin] Add a basic unit testing framework
Previously, we were using JavaScript bindings to gtest to unit test JavaScript code in Gin and Mojo. The gtest bindings were very basic and not very idiomatic. This CL introduces a simple AMD module call "expect" to help us write more idiomatic unit tests. The API for "expect" is based on the popular Jasmine unit testing framework for node.js. I investigated just importing one of Node's many unit testing frameworks, but they all try to do too much (e.g., drive the entire test harness via Node's file system interface). The "expect" modules doesn't try to drive the testing process. We just let gtest handle that. Instead, "expect" just provides a simple language in which to write test assertions. We'll likely evolve our testing strategy over time, but hopefully this CL is an improvement over the primitive gtest bindings. R=jochen@chromium.org TBR=joth@chromium.org BUG=none Review URL: https://codereview.chromium.org/85223002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@237145 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'gin')
-rw-r--r--gin/converter.cc8
-rw-r--r--gin/converter.h2
-rw-r--r--gin/runner.cc5
-rw-r--r--gin/runner.h2
-rw-r--r--gin/runner_unittest.cc2
-rw-r--r--gin/shell/gin_main.cc13
-rw-r--r--gin/test/expect.js289
-rw-r--r--gin/test/file_runner.cc9
-rw-r--r--gin/test/gtest.cc12
-rw-r--r--gin/try_catch.cc32
-rw-r--r--gin/try_catch.h2
11 files changed, 353 insertions, 23 deletions
diff --git a/gin/converter.cc b/gin/converter.cc
index e60e3a0..614e515 100644
--- a/gin/converter.cc
+++ b/gin/converter.cc
@@ -171,5 +171,13 @@ v8::Handle<v8::String> StringToSymbol(v8::Isolate* isolate,
static_cast<uint32_t>(val.length()));
}
+std::string V8ToString(v8::Handle<v8::Value> value) {
+ if (value.IsEmpty())
+ return std::string();
+ std::string result;
+ if (!ConvertFromV8(value, &result))
+ return std::string();
+ return result;
+}
} // namespace gin
diff --git a/gin/converter.h b/gin/converter.h
index 2279319..c22c072 100644
--- a/gin/converter.h
+++ b/gin/converter.h
@@ -162,6 +162,8 @@ bool ConvertFromV8(v8::Handle<v8::Value> input, T* result) {
return Converter<T>::FromV8(input, result);
}
+std::string V8ToString(v8::Handle<v8::Value> value);
+
} // namespace gin
#endif // GIN_CONVERTER_H_
diff --git a/gin/runner.cc b/gin/runner.cc
index 0cd7269..37e16a0 100644
--- a/gin/runner.cc
+++ b/gin/runner.cc
@@ -58,8 +58,9 @@ Runner::Runner(RunnerDelegate* delegate, Isolate* isolate)
Runner::~Runner() {
}
-void Runner::Run(const std::string& script) {
- Run(Script::New(StringToV8(isolate(), script)));
+void Runner::Run(const std::string& source, const std::string& resource_name) {
+ Run(Script::New(StringToV8(isolate(), source),
+ StringToV8(isolate(), resource_name)));
}
void Runner::Run(v8::Handle<Script> script) {
diff --git a/gin/runner.h b/gin/runner.h
index 614b60d..21e656f 100644
--- a/gin/runner.h
+++ b/gin/runner.h
@@ -33,7 +33,7 @@ class Runner : public ContextHolder {
Runner(RunnerDelegate* delegate, v8::Isolate* isolate);
~Runner();
- void Run(const std::string& script);
+ void Run(const std::string& source, const std::string& resource_name);
void Run(v8::Handle<v8::Script> script);
v8::Handle<v8::Value> Call(v8::Handle<v8::Function> function,
diff --git a/gin/runner_unittest.cc b/gin/runner_unittest.cc
index 9c3c30e..9493cf1 100644
--- a/gin/runner_unittest.cc
+++ b/gin/runner_unittest.cc
@@ -25,7 +25,7 @@ TEST(RunnerTest, Run) {
Isolate* isolate = instance.isolate();
Runner runner(&delegate, isolate);
Runner::Scope scope(&runner);
- runner.Run(source);
+ runner.Run(source, "test_data.js");
std::string result;
EXPECT_TRUE(Converter<std::string>::FromV8(
diff --git a/gin/shell/gin_main.cc b/gin/shell/gin_main.cc
index 9d081a3..24fa6df 100644
--- a/gin/shell/gin_main.cc
+++ b/gin/shell/gin_main.cc
@@ -24,11 +24,11 @@ std::string Load(const base::FilePath& path) {
return source;
}
-void Run(base::WeakPtr<Runner> runner, const std::string& source) {
+void Run(base::WeakPtr<Runner> runner, const base::FilePath& path) {
if (!runner)
return;
Runner::Scope scope(runner.get());
- runner->Run(source);
+ runner->Run(Load(path), path.AsUTF8Unsafe());
}
std::vector<base::FilePath> GetModuleSearchPaths() {
@@ -46,7 +46,7 @@ class ShellRunnerDelegate : public ModuleRunnerDelegate {
virtual void UnhandledException(Runner* runner,
TryCatch& try_catch) OVERRIDE {
ModuleRunnerDelegate::UnhandledException(runner, try_catch);
- LOG(ERROR) << try_catch.GetPrettyMessage();
+ LOG(ERROR) << try_catch.GetStackTrace();
}
private:
@@ -68,11 +68,16 @@ int main(int argc, char** argv) {
gin::ShellRunnerDelegate delegate;
gin::Runner runner(&delegate, instance.isolate());
+ {
+ gin::Runner::Scope scope(&runner);
+ v8::V8::SetCaptureStackTraceForUncaughtExceptions(true);
+ }
+
CommandLine::StringVector args = CommandLine::ForCurrentProcess()->GetArgs();
for (CommandLine::StringVector::const_iterator it = args.begin();
it != args.end(); ++it) {
base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(
- gin::Run, runner.GetWeakPtr(), gin::Load(base::FilePath(*it))));
+ gin::Run, runner.GetWeakPtr(), base::FilePath(*it)));
}
message_loop.RunUntilIdle();
diff --git a/gin/test/expect.js b/gin/test/expect.js
new file mode 100644
index 0000000..4154456
--- /dev/null
+++ b/gin/test/expect.js
@@ -0,0 +1,289 @@
+// Copyright 2013 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.
+
+define(["console"], function(console) {
+ // Equality function based on isEqual in
+ // Underscore.js 1.5.2
+ // http://underscorejs.org
+ // (c) 2009-2013 Jeremy Ashkenas,
+ // DocumentCloud,
+ // and Investigative Reporters & Editors
+ // Underscore may be freely distributed under the MIT license.
+ //
+ function has(obj, key) {
+ return obj.hasOwnProperty(key);
+ }
+ function isFunction(obj) {
+ return typeof obj === 'function';
+ }
+ function isArrayBufferClass(className) {
+ return className == '[object ArrayBuffer]' ||
+ className.match(/\[object \w+\d+(Clamped)?Array\]/);
+ }
+ // Internal recursive comparison function for `isEqual`.
+ function eq(a, b, aStack, bStack) {
+ // Identical objects are equal. `0 === -0`, but they aren't identical.
+ // See the Harmony `egal` proposal:
+ // http://wiki.ecmascript.org/doku.php?id=harmony:egal.
+ if (a === b)
+ return a !== 0 || 1 / a == 1 / b;
+ // A strict comparison is necessary because `null == undefined`.
+ if (a == null || b == null)
+ return a === b;
+ // Compare `[[Class]]` names.
+ var className = toString.call(a);
+ if (className != toString.call(b))
+ return false;
+ switch (className) {
+ // Strings, numbers, dates, and booleans are compared by value.
+ case '[object String]':
+ // Primitives and their corresponding object wrappers are equivalent;
+ // thus, `"5"` is equivalent to `new String("5")`.
+ return a == String(b);
+ case '[object Number]':
+ // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is
+ // performed for other numeric values.
+ return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
+ case '[object Date]':
+ case '[object Boolean]':
+ // Coerce dates and booleans to numeric primitive values. Dates are
+ // compared by their millisecond representations. Note that invalid
+ // dates with millisecond representations of `NaN` are not equivalent.
+ return +a == +b;
+ // RegExps are compared by their source patterns and flags.
+ case '[object RegExp]':
+ return a.source == b.source &&
+ a.global == b.global &&
+ a.multiline == b.multiline &&
+ a.ignoreCase == b.ignoreCase;
+ }
+ if (typeof a != 'object' || typeof b != 'object')
+ return false;
+ // Assume equality for cyclic structures. The algorithm for detecting
+ // cyclic structures is adapted from ES 5.1 section 15.12.3, abstract
+ // operation `JO`.
+ var length = aStack.length;
+ while (length--) {
+ // Linear search. Performance is inversely proportional to the number of
+ // unique nested structures.
+ if (aStack[length] == a)
+ return bStack[length] == b;
+ }
+ // Objects with different constructors are not equivalent, but `Object`s
+ // from different frames are.
+ var aCtor = a.constructor, bCtor = b.constructor;
+ if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
+ isFunction(bCtor) && (bCtor instanceof bCtor))
+ && ('constructor' in a && 'constructor' in b)) {
+ return false;
+ }
+ // Add the first object to the stack of traversed objects.
+ aStack.push(a);
+ bStack.push(b);
+ var size = 0, result = true;
+ // Recursively compare objects and arrays.
+ if (className == '[object Array]' || isArrayBufferClass(className)) {
+ // Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length;
+ result = size == b.length;
+ if (result) {
+ // Deep compare the contents, ignoring non-numeric properties.
+ while (size--) {
+ if (!(result = eq(a[size], b[size], aStack, bStack)))
+ break;
+ }
+ }
+ } else {
+ // Deep compare objects.
+ for (var key in a) {
+ if (has(a, key)) {
+ // Count the expected number of properties.
+ size++;
+ // Deep compare each member.
+ if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack)))
+ break;
+ }
+ }
+ // Ensure that both objects contain the same number of properties.
+ if (result) {
+ for (key in b) {
+ if (has(b, key) && !(size--))
+ break;
+ }
+ result = !size;
+ }
+ }
+ // Remove the first object from the stack of traversed objects.
+ aStack.pop();
+ bStack.pop();
+ return result;
+ };
+
+ function describe(subjects) {
+ var descriptions = [];
+ Object.getOwnPropertyNames(subjects).forEach(function(name) {
+ if (name === "Description")
+ descriptions.push(subjects[name]);
+ else
+ descriptions.push(name + ": " + JSON.stringify(subjects[name]));
+ });
+ return descriptions.join(" ");
+ }
+
+ var predicates = {};
+
+ predicates.toBe = function(actual, expected) {
+ return {
+ "result": actual === expected,
+ "message": describe({
+ "Actual": actual,
+ "Expected": expected,
+ }),
+ };
+ };
+
+ predicates.toEqual = function(actual, expected) {
+ return {
+ "result": eq(actual, expected, [], []),
+ "message": describe({
+ "Actual": actual,
+ "Expected": expected,
+ }),
+ };
+ };
+
+ predicates.toBeDefined = function(actual) {
+ return {
+ "result": typeof actual !== "undefined",
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected a defined value",
+ }),
+ };
+ };
+
+ predicates.toBeUndefined = function(actual) {
+ // Recall: undefined is just a global variable. :)
+ return {
+ "result": typeof actual === "undefined",
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected an undefined value",
+ }),
+ };
+ };
+
+ predicates.toBeNull = function(actual) {
+ // Recall: typeof null === "object".
+ return {
+ "result": actual === null,
+ "message": describe({
+ "Actual": actual,
+ "Expected": null,
+ }),
+ };
+ };
+
+ predicates.toBeTruthy = function(actual) {
+ return {
+ "result": !!actual,
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected a truthy value",
+ }),
+ };
+ };
+
+ predicates.toBeFalsy = function(actual) {
+ return {
+ "result": !!!actual,
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected a falsy value",
+ }),
+ };
+ };
+
+ predicates.toContain = function(actual, element) {
+ return {
+ "result": (function () {
+ for (var i = 0; i < actual.length; ++i) {
+ if (eq(actual[i], element, [], []))
+ return true;
+ }
+ return false;
+ })(),
+ "message": describe({
+ "Actual": actual,
+ "Element": element,
+ }),
+ };
+ };
+
+ predicates.toBeLessThan = function(actual, reference) {
+ return {
+ "result": actual < reference,
+ "message": describe({
+ "Actual": actual,
+ "Reference": reference,
+ }),
+ };
+ };
+
+ predicates.toBeGreaterThan = function(actual, reference) {
+ return {
+ "result": actual > reference,
+ "message": describe({
+ "Actual": actual,
+ "Reference": reference,
+ }),
+ };
+ };
+
+ predicates.toThrow = function(actual) {
+ return {
+ "result": (function () {
+ if (!isFunction(actual))
+ throw new TypeError;
+ try {
+ actual();
+ } catch (ex) {
+ return true;
+ }
+ return false;
+ })(),
+ "message": "Expected function to throw",
+ };
+ }
+
+ function negate(predicate) {
+ return function() {
+ var outcome = predicate.apply(null, arguments);
+ outcome.result = !outcome.result;
+ return outcome;
+ }
+ }
+
+ function check(predicate) {
+ return function() {
+ var outcome = predicate.apply(null, arguments);
+ if (outcome.result)
+ return;
+ throw outcome.message;
+ };
+ }
+
+ function Condition(actual) {
+ this.not = {};
+ Object.getOwnPropertyNames(predicates).forEach(function(name) {
+ var bound = predicates[name].bind(null, actual);
+ this[name] = check(bound);
+ this.not[name] = check(negate(bound));
+ }, this);
+ }
+
+ return function(actual) {
+ return new Condition(actual);
+ };
+});
diff --git a/gin/test/file_runner.cc b/gin/test/file_runner.cc
index 0c60c4b..2bad0b2 100644
--- a/gin/test/file_runner.cc
+++ b/gin/test/file_runner.cc
@@ -41,7 +41,7 @@ FileRunnerDelegate::~FileRunnerDelegate() {
void FileRunnerDelegate::UnhandledException(Runner* runner,
TryCatch& try_catch) {
ModuleRunnerDelegate::UnhandledException(runner, try_catch);
- EXPECT_FALSE(try_catch.HasCaught()) << try_catch.GetPrettyMessage();
+ FAIL() << try_catch.GetStackTrace();
}
void RunTestFromFile(const base::FilePath& path, FileRunnerDelegate* delegate) {
@@ -55,15 +55,14 @@ void RunTestFromFile(const base::FilePath& path, FileRunnerDelegate* delegate) {
gin::Runner runner(delegate, instance.isolate());
{
gin::Runner::Scope scope(&runner);
- runner.Run(source);
+ v8::V8::SetCaptureStackTraceForUncaughtExceptions(true);
+ runner.Run(source, path.AsUTF8Unsafe());
message_loop.RunUntilIdle();
v8::Handle<v8::Value> result = runner.context()->Global()->Get(
StringToSymbol(runner.isolate(), "result"));
- std::string result_string;
- ASSERT_TRUE(ConvertFromV8(result, &result_string));
- EXPECT_EQ("PASS", result_string);
+ EXPECT_EQ("PASS", V8ToString(result));
}
}
diff --git a/gin/test/gtest.cc b/gin/test/gtest.cc
index 422d3ec..254c735 100644
--- a/gin/test/gtest.cc
+++ b/gin/test/gtest.cc
@@ -18,6 +18,16 @@ namespace gin {
namespace {
+void Fail(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ Arguments args(info);
+
+ std::string description;
+ if (!args.GetNext(&description))
+ return args.ThrowError();
+
+ FAIL() << description;
+}
+
void ExpectTrue(bool condition, const std::string& description) {
EXPECT_TRUE(condition) << description;
}
@@ -48,6 +58,8 @@ v8::Local<v8::ObjectTemplate> GTest::GetTemplate(v8::Isolate* isolate) {
data->GetObjectTemplate(&g_wrapper_info);
if (templ.IsEmpty()) {
templ = v8::ObjectTemplate::New();
+ templ->Set(StringToSymbol(isolate, "fail"),
+ v8::FunctionTemplate::New(Fail));
templ->Set(StringToSymbol(isolate, "expectTrue"),
CreateFunctionTempate(isolate, base::Bind(ExpectTrue)));
templ->Set(StringToSymbol(isolate, "expectFalse"),
diff --git a/gin/try_catch.cc b/gin/try_catch.cc
index 302d8bd..89a969f 100644
--- a/gin/try_catch.cc
+++ b/gin/try_catch.cc
@@ -4,6 +4,9 @@
#include "gin/try_catch.h"
+#include <sstream>
+
+#include "base/logging.h"
#include "gin/converter.h"
namespace gin {
@@ -18,15 +21,26 @@ bool TryCatch::HasCaught() {
return try_catch_.HasCaught();
}
-std::string TryCatch::GetPrettyMessage() {
- std::string info;
- ConvertFromV8(try_catch_.Message()->Get(), &info);
-
- std::string sounce_line;
- if (ConvertFromV8(try_catch_.Message()->GetSourceLine(), &sounce_line))
- info += "\n" + sounce_line;
-
- return info;
+std::string TryCatch::GetStackTrace() {
+ std::stringstream ss;
+ v8::Handle<v8::Message> message = try_catch_.Message();
+ ss << V8ToString(message->Get()) << std::endl
+ << V8ToString(message->GetSourceLine()) << std::endl;
+
+ v8::Handle<v8::StackTrace> trace = message->GetStackTrace();
+ if (trace.IsEmpty())
+ return ss.str();
+
+ int len = trace->GetFrameCount();
+ for (int i = 0; i < len; ++i) {
+ v8::Handle<v8::StackFrame> frame = trace->GetFrame(i);
+ ss << V8ToString(frame->GetScriptName()) << ":"
+ << frame->GetLineNumber() << ":"
+ << frame->GetColumn() << ": "
+ << V8ToString(frame->GetFunctionName())
+ << std::endl;
+ }
+ return ss.str();
}
} // namespace gin
diff --git a/gin/try_catch.h b/gin/try_catch.h
index 8026b3b..43f68ac 100644
--- a/gin/try_catch.h
+++ b/gin/try_catch.h
@@ -18,7 +18,7 @@ class TryCatch {
~TryCatch();
bool HasCaught();
- std::string GetPrettyMessage();
+ std::string GetStackTrace();
private:
v8::TryCatch try_catch_;