Skip to content

Commit

Permalink
Merge pull request #182 from gsamokovarov/console-in-kernel
Browse files Browse the repository at this point in the history
Let #console live in Kernel
  • Loading branch information
gsamokovarov committed Jan 24, 2016
2 parents c22e550 + 8f308d8 commit 014f4a4
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 95 deletions.
1 change: 0 additions & 1 deletion lib/web_console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ module WebConsole
extend ActiveSupport::Autoload

autoload :View
autoload :Helper
autoload :Evaluator
autoload :Session
autoload :Response
Expand Down
56 changes: 39 additions & 17 deletions lib/web_console/extensions.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
ActionDispatch::DebugExceptions.class_eval do
def render_exception_with_web_console(request, exception)
render_exception_without_web_console(request, exception).tap do
# Retain superficial Rails 4.2 compatibility.
env = Hash === request ? request : request.env
module Kernel
# Instructs Web Console to render a console in the specified binding.
#
# If +bidning+ isn't explicitly given it will default to the binding of the
# previous frame. E.g. the one that invoked +console+.
#
# Raises DoubleRenderError if a double +console+ invocation per request is
# detected.
def console(binding = WebConsole.caller_bindings.first)
raise WebConsole::DoubleRenderError if Thread.current[:__web_console_binding]

backtrace_cleaner = env['action_dispatch.backtrace_cleaner']
error = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).exception
Thread.current[:__web_console_binding] = binding

# Get the original exception if ExceptionWrapper decides to follow it.
env['web_console.exception'] = error
# Make sure nothing is rendered from the view helper. Otherwise
# you're gonna see unexpected #<Binding:0x007fee4302b078> in the
# templates.
nil
end
end

module ActionDispatch
class DebugExceptions
def render_exception_with_web_console(request, exception)
render_exception_without_web_console(request, exception).tap do
# Retain superficial Rails 4.2 compatibility.
env = Hash === request ? request : request.env

backtrace_cleaner = env['action_dispatch.backtrace_cleaner']
error = ExceptionWrapper.new(backtrace_cleaner, exception).exception

# ActionView::Template::Error bypass ExceptionWrapper original
# exception following. The backtrace in the view is generated from
# reaching out to original_exception in the view.
if error.is_a?(ActionView::Template::Error)
env['web_console.exception'] = error.cause
# Get the original exception if ExceptionWrapper decides to follow it.
Thread.current[:__web_console_exception] = error

# ActionView::Template::Error bypass ExceptionWrapper original
# exception following. The backtrace in the view is generated from
# reaching out to original_exception in the view.
if error.is_a?(ActionView::Template::Error)
Thread.current[:__web_console_exception] = error.cause
end
end
end
end

alias_method :render_exception_without_web_console, :render_exception
alias_method :render_exception, :render_exception_with_web_console
alias_method :render_exception_without_web_console, :render_exception
alias_method :render_exception, :render_exception_with_web_console
end
end
22 changes: 0 additions & 22 deletions lib/web_console/helper.rb

This file was deleted.

13 changes: 6 additions & 7 deletions lib/web_console/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,7 @@ def call(env)

status, headers, body = call_app(env)

if exception = env['web_console.exception']
session = Session.from_exception(exception)
elsif binding = env['web_console.binding']
session = Session.from_binding(binding)
end

if session && acceptable_content_type?(headers)
if session = Session.from(Thread.current) and acceptable_content_type?(headers)
response = Response.new(body, status, headers)
template = Template.new(env, session)

Expand All @@ -49,6 +43,11 @@ def call(env)
WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
raise e
ensure
# Clean up the fiber locals after the session creation. Object#console
# uses those to communicate the current binding or exception to the middleware.
Thread.current[:__web_console_exception] = nil
Thread.current[:__web_console_binding] = nil

raise app_exception if Exception === app_exception
end

Expand Down
8 changes: 0 additions & 8 deletions lib/web_console/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ class Railtie < ::Rails::Railtie
require 'web_console/integration'
require 'web_console/extensions'

ActiveSupport.on_load(:action_view) do
ActionView::Base.send(:include, Helper)
end

ActiveSupport.on_load(:action_controller) do
ActionController::Base.send(:include, Helper)
end

if logger = ::Rails.logger
WebConsole.logger = logger
end
Expand Down
27 changes: 16 additions & 11 deletions lib/web_console/session.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module WebConsole
# A session lets you persist wrap an +Evaluator+ instance in memory
# associated with multiple bindings.
# A session lets you persist an +Evaluator+ instance in memory associated
# with multiple bindings.
#
# Each newly created session is persisted into memory and you can find it
# later its +id+.
# later by its +id+.
#
# A session may be associated with multiple bindings. This is used by the
# error pages only, as currently, this is the only client that needs to do
Expand All @@ -21,14 +21,19 @@ def find(id)
inmemory_storage[id]
end

# Create a Session from an exception.
def from_exception(exc)
new(exc.bindings)
end

# Create a Session from a single binding.
def from_binding(binding)
new(binding)
# Create a Session from an binding or exception in a storage.
#
# The storage is expected to respond to #[]. The binding is expected in
# :__web_console_binding and the exception in :__web_console_exception.
#
# Can return nil, if no binding or exception have been preserved in the
# storage.
def from(storage)
if exc = storage[:__web_console_exception]
new(exc.bindings)
elsif binding = storage[:__web_console_binding]
new(binding)
end
end
end

Expand Down
16 changes: 16 additions & 0 deletions test/dummy/app/controllers/model_test_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class ModelTestController < ApplicationController
def index
LocalModel.new.work
end

class LocalModel
def initialize
@state = :state
end

def work
local_var = 42
console
end
end
end
Empty file.
1 change: 1 addition & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
get :exception_test, to: "exception_test#index"
get :xhr_test, to: "exception_test#xhr"
get :helper_test, to: "helper_test#index"
get :model_test, to: "model_test#index"
get :helper_error, to: "helper_error#index"
get :controller_helper_test, to: "controller_helper_test#index"

Expand Down
4 changes: 2 additions & 2 deletions test/web_console/extensions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ def call(env)
@app = DebugExceptions.new(Application.new)
end

test "follows ActionView::Template::Error original error in env['web_console.exception']" do
test "follows ActionView::Template::Error original error in Thread.current[:__web_console_exception]" do
get "/", params: {}, headers: {
'action_dispatch.show_detailed_exceptions' => true,
'action_dispatch.show_exceptions' => true,
'action_dispatch.logger' => Logger.new(StringIO.new)
}

assert_equal 42, request.env['web_console.exception'].bindings.first.eval('@ivar')
assert_equal 42, Thread.current[:__web_console_exception].bindings.first.eval('@ivar')
end
end
end
5 changes: 3 additions & 2 deletions test/web_console/helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
module WebConsole
class HelperTest < ActionDispatch::IntegrationTest
class BaseApplication
include Helper

def call(env)
[ status, headers, body ]
end
Expand Down Expand Up @@ -59,6 +57,9 @@ def call(env)
end

setup do
Thread.current[:__web_console_exception] = nil
Thread.current[:__web_console_binding] = nil

Request.stubs(:whitelisted_ips).returns(IPAddr.new('0.0.0.0/0'))

@app = Middleware.new(SingleConsoleApplication.new)
Expand Down
43 changes: 24 additions & 19 deletions test/web_console/middleware_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,59 +43,64 @@ def body
end

test 'render console in an html application from web_console.binding' do
get '/', params: nil, headers: { 'web_console.binding' => binding }
Thread.current[:__web_console_binding] = binding

get '/', params: nil

assert_select '#console'
end

test 'render console in an html application from web_console.exception' do
get '/', params: nil, headers: { 'web_console.exception' => raise_exception }
Thread.current[:__web_console_exception] = raise_exception

get '/', params: nil

assert_select 'body > #console'
end

test 'render console if response format is HTML' do
Thread.current[:__web_console_binding] = binding
@app = Middleware.new(Application.new(response_content_type: Mime[:html]))
get '/', params: nil, headers: { 'web_console.binding' => binding }

get '/', params: nil

assert_select '#console'
end

test 'does not render console if response format is not HTML' do
Thread.current[:__web_console_binding] = binding
@app = Middleware.new(Application.new(response_content_type: Mime[:json]))
get '/', params: nil, headers: { 'web_console.binding' => binding }

get '/', params: nil

assert_select '#console', 0
end

test 'returns X-Web-Console-Session-Id as response header' do
get '/', params: nil, headers: { 'web_console.binding' => binding }
Thread.current[:__web_console_binding] = binding

get '/', params: nil

session_id = response.headers["X-Web-Console-Session-Id"]

assert_not Session.find(session_id).nil?
end

test 'prioritizes web_console.exception over web_console.binding' do
exception = raise_exception

Session.expects(:from_exception).with(exception)

get '/', params: nil, headers: { 'web_console.binding' => binding, 'web_console.exception' => exception }
end

test "doesn't render console in non html response" do
Thread.current[:__web_console_binding] = binding
@app = Middleware.new(Application.new(response_content_type: Mime[:json]))
get '/', params: nil, headers: { 'web_console.binding' => binding }

get '/', params: nil

assert_select '#console', 0
end

test "doesn't render console from non whitelisted IP" do
Thread.current[:__web_console_binding] = binding
Request.stubs(:whitelisted_ips).returns(IPAddr.new('127.0.0.1'))

silence(:stderr) do
get '/', params: nil, headers: { 'REMOTE_ADDR' => '1.1.1.1', 'web_console.binding' => binding }
get '/', params: nil, headers: { 'REMOTE_ADDR' => '1.1.1.1' }
end

assert_select '#console', 0
Expand All @@ -110,9 +115,9 @@ def body
test 'can evaluate code and return it as a JSON' do
session, line = Session.new(binding), __LINE__

Session.stubs(:from_binding).returns(session)
Session.stubs(:from).returns(session)

get '/', params: nil, headers: { 'web-console.binding' => binding }
get '/', params: nil
put "/repl_sessions/#{session.id}", xhr: true, params: { input: '__LINE__' }

assert_equal({ output: "=> #{line}\n" }.to_json, response.body)
Expand All @@ -121,9 +126,9 @@ def body
test 'can switch bindings on error pages' do
session = Session.new(exception = raise_exception)

Session.stubs(:from_exception).returns(session)
Session.stubs(:from).returns(session)

get '/', params: nil, headers: { 'web-console.exception' => exception }
get '/', params: nil
post "/repl_sessions/#{session.id}/trace", xhr: true, params: { frame_id: 1 }

assert_equal({ ok: true }.to_json, response.body)
Expand Down
24 changes: 18 additions & 6 deletions test/web_console/session_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,36 @@ def initialize(line)
assert_equal "=> 42\n", @session.eval('40 + 2')
end

test 'can create session from a single binding' do
test '#from can create session from a single binding' do
saved_line, saved_binding = __LINE__, binding
session = Session.from_binding(saved_binding)
Thread.current[:__web_console_binding] = saved_binding

session = Session.from(__web_console_binding: saved_binding)

assert_equal "=> #{saved_line}\n", session.eval('__LINE__')
end

test 'can create session from an exception' do
test '#from can create session from an exception' do
exc = LineAwareError.raise
session = Session.from_exception(exc)

session = Session.from(__web_console_exception: exc)

assert_equal "=> #{exc.line}\n", session.eval('__LINE__')
end

test 'can switch to bindings' do
test '#from can switch to bindings' do
exc, saved_line = LineAwareError.raise, __LINE__

session = Session.from(__web_console_exception: exc)
session.switch_binding_to(1)

assert_equal "=> #{saved_line}\n", session.eval('__LINE__')
end

test '#from prioritizes exceptions over bindings' do
exc, saved_line = LineAwareError.raise, __LINE__

session = Session.from_exception(exc)
session = Session.from(__web_console_exception: exc, __web_console_binding: binding)
session.switch_binding_to(1)

assert_equal "=> #{saved_line}\n", session.eval('__LINE__')
Expand Down

0 comments on commit 014f4a4

Please sign in to comment.