Skip to content

Commit

Permalink
Merge pull request #265 from yuki24/support-for-nested-exceptions
Browse files Browse the repository at this point in the history
Add support for nested exceptions
  • Loading branch information
gsamokovarov authored Apr 19, 2019
2 parents bba90ba + 3c30990 commit b249558
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 19 deletions.
19 changes: 19 additions & 0 deletions lib/web_console/exception_mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/web_console/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions lib/web_console/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@ 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

# 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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion lib/web_console/templates/console.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 4 additions & 3 deletions lib/web_console/templates/error_page.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
25 changes: 21 additions & 4 deletions test/web_console/middleware_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -178,18 +178,35 @@ 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"])
end

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 "/"
Expand Down
23 changes: 20 additions & 3 deletions test/web_console/session_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down

0 comments on commit b249558

Please sign in to comment.