From 3c30990344480b6c11c6c5c70ccd0ad96b828c5f Mon Sep 17 00:00:00 2001 From: Yuki Nishijima Date: Sun, 8 Jul 2018 17:55:22 -0400 Subject: [PATCH] Add support for nested exceptions --- lib/web_console/exception_mapper.rb | 19 ++++++++++++++++ lib/web_console/middleware.rb | 2 +- lib/web_console/session.rb | 17 ++++++++------ lib/web_console/templates/console.js.erb | 6 ++++- lib/web_console/templates/error_page.js.erb | 7 +++--- test/web_console/middleware_test.rb | 25 +++++++++++++++++---- test/web_console/session_test.rb | 23 ++++++++++++++++--- 7 files changed, 80 insertions(+), 19 deletions(-) diff --git a/lib/web_console/exception_mapper.rb b/lib/web_console/exception_mapper.rb index 9042e770..e750fc3a 100644 --- a/lib/web_console/exception_mapper.rb +++ b/lib/web_console/exception_mapper.rb @@ -2,9 +2,28 @@ module WebConsole class ExceptionMapper + attr_reader :exc + + def self.follow(exc) + mappers = [new(exc)] + + while cause = (cause || exc).cause + mappers << new(cause) + end + + mappers + end + + def self.find_binding(mappers, exception_object_id) + mappers.detect do |exception_mapper| + exception_mapper.exc.object_id == exception_object_id.to_i + end || mappers.first + end + def initialize(exception) @backtrace = exception.backtrace @bindings = exception.bindings + @exc = exception end def first diff --git a/lib/web_console/middleware.rb b/lib/web_console/middleware.rb index 0e694cfe..93320ee0 100644 --- a/lib/web_console/middleware.rb +++ b/lib/web_console/middleware.rb @@ -110,7 +110,7 @@ def update_repl_session(id, request) def change_stack_trace(id, request) json_response_with_session(id, request) do |session| - session.switch_binding_to(request.params[:frame_id]) + session.switch_binding_to(request.params[:frame_id], request.params[:exception_object_id]) { ok: true } end diff --git a/lib/web_console/session.rb b/lib/web_console/session.rb index 788f2f33..58c0e7ce 100644 --- a/lib/web_console/session.rb +++ b/lib/web_console/session.rb @@ -31,9 +31,9 @@ def find(id) # storage. def from(storage) if exc = storage[:__web_console_exception] - new(ExceptionMapper.new(exc)) + new(ExceptionMapper.follow(exc)) elsif binding = storage[:__web_console_binding] - new([binding]) + new([[binding]]) end end end @@ -41,10 +41,11 @@ def from(storage) # An unique identifier for every REPL. attr_reader :id - def initialize(bindings) + def initialize(exception_mappers) @id = SecureRandom.hex(16) - @bindings = bindings - @evaluator = Evaluator.new(@current_binding = bindings.first) + + @exception_mappers = exception_mappers + @evaluator = Evaluator.new(@current_binding = exception_mappers.first.first) store_into_memory end @@ -59,8 +60,10 @@ def eval(input) # Switches the current binding to the one at specified +index+. # # Returns nothing. - def switch_binding_to(index) - @evaluator = Evaluator.new(@current_binding = @bindings[index.to_i]) + def switch_binding_to(index, exception_object_id) + bindings = ExceptionMapper.find_binding(@exception_mappers, exception_object_id) + + @evaluator = Evaluator.new(@current_binding = bindings[index.to_i]) end # Returns context of the current binding diff --git a/lib/web_console/templates/console.js.erb b/lib/web_console/templates/console.js.erb index ef5837ad..388aaad5 100644 --- a/lib/web_console/templates/console.js.erb +++ b/lib/web_console/templates/console.js.erb @@ -871,10 +871,14 @@ REPLConsole.prototype.scrollToBottom = function() { }; // Change the binding of the console. -REPLConsole.prototype.switchBindingTo = function(frameId, callback) { +REPLConsole.prototype.switchBindingTo = function(frameId, exceptionObjectId, callback) { var url = this.getSessionUrl('trace'); var params = "frame_id=" + encodeURIComponent(frameId); + if (exceptionObjectId) { + params = params + "&exception_object_id=" + encodeURIComponent(exceptionObjectId); + } + var _this = this; postRequest(url, params, function() { var text = "Context has changed to: " + callback(); diff --git a/lib/web_console/templates/error_page.js.erb b/lib/web_console/templates/error_page.js.erb index 2363c376..9fc91d26 100644 --- a/lib/web_console/templates/error_page.js.erb +++ b/lib/web_console/templates/error_page.js.erb @@ -8,9 +8,10 @@ for (var i = 0; i < traceFrames.length; i++) { e.preventDefault(); var target = e.target; var frameId = target.dataset.frameId; + var exceptionObjectId = target.dataset.exceptionObjectId; // Change the binding of the console. - changeBinding(frameId, function() { + changeBinding(frameId, exceptionObjectId, function() { // Rails already handles toggling the select class selectedFrame = target; return target.innerHTML; @@ -22,8 +23,8 @@ for (var i = 0; i < traceFrames.length; i++) { } // Change the binding of the current session and prompt the user. -function changeBinding(frameId, callback) { - REPLConsole.currentSession.switchBindingTo(frameId, callback); +function changeBinding(frameId, exceptionObjectId, callback) { + REPLConsole.currentSession.switchBindingTo(frameId, exceptionObjectId, callback); } function changeSourceExtract(frameId) { diff --git a/test/web_console/middleware_test.rb b/test/web_console/middleware_test.rb index a40f538f..64f21164 100644 --- a/test/web_console/middleware_test.rb +++ b/test/web_console/middleware_test.rb @@ -157,7 +157,7 @@ def headers end test "can evaluate code and return it as a JSON" do - session, line = Session.new([binding]), __LINE__ + session, line = Session.new([[binding]]), __LINE__ Session.stubs(:from).returns(session) @@ -168,7 +168,7 @@ def headers end test "can switch bindings on error pages" do - session = Session.new(raise_exception.bindings) + session = Session.new([WebConsole::ExceptionMapper.new(raise_exception)]) Session.stubs(:from).returns(session) @@ -178,10 +178,27 @@ def headers assert_equal({ ok: true }.to_json, response.body) end + test "can switch to the cause on error pages" do + nested_error = begin + raise "First error" + rescue + raise "Second Error" rescue $! + end + + session = Session.new(WebConsole::ExceptionMapper.follow(nested_error)) + + Session.stubs(:from).returns(session) + + get "/", params: nil + post "/repl_sessions/#{session.id}/trace", xhr: true, params: { frame_id: 1, exception_object_id: nested_error.cause.object_id } + + assert_equal({ ok: true }.to_json, response.body) + end + test "can be changed mount point" do Middleware.mount_point = "/customized/path" - session, value = Session.new([binding]), __LINE__ + session, value = Session.new([[binding]]), __LINE__ put "/customized/path/repl_sessions/#{session.id}", params: { input: "value" }, xhr: true assert_equal("=> #{value}\n", JSON.parse(response.body)["output"]) @@ -189,7 +206,7 @@ def headers test "can return context information by passing a context param" do hello = hello = "world" - session = Session.new([binding]) + session = Session.new([[binding]]) Session.stubs(:from).returns(session) get "/" diff --git a/test/web_console/session_test.rb b/test/web_console/session_test.rb index 68b377b8..b407e8f5 100644 --- a/test/web_console/session_test.rb +++ b/test/web_console/session_test.rb @@ -11,6 +11,13 @@ def self.raise(value) exc end + def self.raise_nested_error(value) + ::Kernel.raise self, value + rescue + value = 1 # Override value so we can target the binding here + ::Kernel.raise "Second Error" rescue $! + end + attr_reader :value def initialize(value) @@ -20,7 +27,7 @@ def initialize(value) setup do Session.inmemory_storage.clear - @session = Session.new([binding]) + @session = Session.new([[binding]]) end test "returns nil when a session is not found" do @@ -47,7 +54,7 @@ def eval(string) self end - session = Session.new([binding]) + session = Session.new([[binding]]) assert_equal session.eval("called?"), "=> \"yes\"\n" end @@ -74,7 +81,17 @@ def eval(string) exc = ValueAwareError.raise(value) session = Session.from(__web_console_exception: exc) - session.switch_binding_to(1) + session.switch_binding_to(1, exc.object_id) + + assert_equal "=> #{value}\n", session.eval("value") + end + + test "#from can switch to the cause" do + value = __LINE__ + exc = ValueAwareError.raise_nested_error(value) + + session = Session.from(__web_console_exception: exc) + session.switch_binding_to(1, exc.cause.object_id) assert_equal "=> #{value}\n", session.eval("value") end