From f34cc5247a01cd4dcebe4ade40d72556f7642932 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Sun, 12 Aug 2012 21:25:48 -0500 Subject: [PATCH] support objects with messages as error values --- lib/v8.rb | 1 + lib/v8/error.rb | 7 ++ lib/v8/stack.rb | 66 ++++++++++++++++ spec/v8/error_spec.rb | 177 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 lib/v8/stack.rb diff --git a/lib/v8.rb b/lib/v8.rb index d74790f..892550a 100644 --- a/lib/v8.rb +++ b/lib/v8.rb @@ -3,6 +3,7 @@ require "v8/version" require 'v8/weak' require 'v8/init' require 'v8/error' +require 'v8/stack' require 'v8/conversion/fundamental' require 'v8/conversion/indentity' require 'v8/conversion/reference' diff --git a/lib/v8/error.rb b/lib/v8/error.rb index 6681c58..e406eae 100644 --- a/lib/v8/error.rb +++ b/lib/v8/error.rb @@ -1,5 +1,10 @@ module V8 class Error < StandardError + + # capture 99 stack frames on exception with normal details. + # You can adjust these values for performance or turn of stack capture entirely + V8::C::V8::SetCaptureStackTraceForUncaughtExceptions(true, 99, V8::C::StackTrace::kOverview) + attr_reader :value def initialize(message, value) super(message) @@ -51,6 +56,8 @@ module V8 else raise V8::Error.new(exception.Get("message").to_ruby, value) end + elsif exception.IsObject() + raise V8::Error.new(value['message'] || value.to_s, value) else raise V8::Error.new(exception.ToString().to_ruby, value) end diff --git a/lib/v8/stack.rb b/lib/v8/stack.rb new file mode 100644 index 0000000..e8e9be9 --- /dev/null +++ b/lib/v8/stack.rb @@ -0,0 +1,66 @@ + +module V8 + + class StackTrace + include Enumerable + + def initialize(native) + @native = native + end + + def length + @native ? @native.GetFrameCount() : 0 + end + + def each + return unless @native + for i in 0..length - 1 + yield V8::StackFrame.new(@native.GetFrame(i)) + end + end + + def to_s + @native ? map(&:to_s).join("\n") : "" + end + end + + class StackFrame + + def initialize(native) + @context = V8::Context.current + @native = native + end + + def script_name + @context.to_ruby(@native.GetScriptName()) + end + + def function_name + @context.to_ruby(@native.GetFunctionName()) + end + + def line_number + @native.GetLineNumber() + end + + def column + @native.GetColumn() + end + + def eval? + @native.IsEval() + end + + def constructor? + @native.IsConstructor() + end + + def to_s + "at " + if !function_name.empty? + "#{function_name} (#{script_name}:#{line_number}:#{column})" + else + "#{script_name}:#{line_number}:#{column}" + end + end + end +end \ No newline at end of file diff --git a/spec/v8/error_spec.rb b/spec/v8/error_spec.rb index d2a3fb7..bb3984e 100644 --- a/spec/v8/error_spec.rb +++ b/spec/v8/error_spec.rb @@ -1,21 +1,168 @@ require 'spec_helper' +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + describe V8::Error do - it "uses the same ruby exception through multiple language boundaries" do - V8::Context.new do |cxt| - error = StandardError.new('potato') - cxt['one'] = lambda do - cxt.eval('two()', 'one.js') - end - cxt['two'] = lambda do - cxt.eval('three()', 'two.js') - end - cxt['three'] = lambda do - raise error - end - lambda { - cxt.eval('one()') - }.should raise_error {|e| e.should be error} + + before do + @cxt = V8::Context.new + @cxt['one'] = lambda do + @cxt.eval('two()', 'one.js') + end + @cxt['two'] = lambda do + @cxt.eval('three()', 'two.js') end end + + it "captures a message without over nesting when the error is an error" do + throw! do |e| + e.message.should == "BOOM!" + end + end + + it "captures the js message without over nesting when the error is a normal object" do + throw!('{foo: "bar"}') do |e| + e.message.should == "[object Object]" + end + throw!('{message: "bar"}') do |e| + e.message.should == "bar" + end + end + + it "captures a thrown value as the message" do + throw!('"BOOM!"') do |e| + e.message.should == "BOOM!" + end + throw!('6') do |e| + e.message.should == '6' + end + end + + it "has a reference to the root javascript cause" do + pending + throw!('"I am a String"') do |e| + e.should_not be_in_ruby + e.should be_in_javascript + e.value.should == "I am a String" + end + end + + it "has a reference to the root ruby cause if one exists" do + pending + StandardError.new("BOOM!").tap do |bomb| + @cxt['boom'] = lambda do + raise bomb + end + lambda { + @cxt.eval('boom()', 'boom.js') + }.should(raise_error do |raised| + raised.should be_in_ruby + raised.should_not be_in_javascript + raised.value.should be(bomb) + end) + end + end + + describe "backtrace" do + before {pending} + it "is mixed with ruby and javascript" do + throw! do |e| + e.backtrace.first.should == "at three.js:1:7" + e.backtrace[1].should =~ /error_spec.rb/ + e.backtrace[2].should == "at two.js:1:1" + e.backtrace[3].should =~ /error_spec.rb/ + e.backtrace[4].should == "at one.js:1:1" + end + end + + it "can be set to show only ruby frames" do + throw! do |e| + e.backtrace(:ruby).each do |frame| + frame.should =~ /(\.rb|):\d+/ + end + end + end + + it "can be set to show only javascript frames" do + throw! do |e| + e.backtrace(:javascript).each do |frame| + frame.should =~ /\.js:\d:\d/ + end + end + end + + it "includes a mystery marker when the original frame is unavailable because what got thrown wasn't an error" do + throw!("6") do |e| + e.backtrace.first.should == 'at three.js:1:1' + end + end + + it "has a source name and line number when there is a javascript SyntaxError" do + lambda do + @cxt.eval(<<-INVALID, 'source.js') +"this line is okay"; +"this line has a syntax error because it ends with a colon": +"this line is also okay"; +"how do I find out that line 2 has the syntax error?"; +INVALID + end.should raise_error(V8::JSError) {|error| + error.backtrace.first.should == 'at source.js:2:60' + } + end + + it "can start with ruby at the bottom" do + @cxt['boom'] = lambda do + raise StandardError, "Bif!" + end + lambda { + @cxt.eval('boom()', "boom.js") + }.should(raise_error {|e| + e.backtrace.first.should =~ /error_spec\.rb/ + e.backtrace[1].should =~ /boom.js/ + }) + end + end + + + def throw!(js = "new Error('BOOM!')", &block) + @cxt['three'] = lambda do + @cxt.eval("throw #{js}", 'three.js') + end + lambda do + @cxt['one'].call() + end.should(raise_error(V8::JSError, &block)) + end end + + +# describe V8::Error do +# describe "A ruby exception thrown inside JavaScript" do +# before do +# @error = StandardError.new('potato') +# begin +# V8::Context.new do |cxt| +# cxt['one'] = lambda do +# cxt.eval('two()', 'one.js') +# end +# cxt['two'] = lambda do +# cxt.eval('three()', 'two.js') +# end +# cxt['three'] = lambda do +# raise @error +# end +# cxt.eval('one()') +# end +# rescue StandardError => e +# @thrown = e +# end +# end +# it "is raised up through the call stack" do +# @thrown.should be(@error) +# end +# +# it "shows both the javascript and the ruby callframes" do +# puts @error.backtrace.join('
') +# end +# +# end +# end