Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let #console live in Kernel #182

Merged
merged 1 commit into from
Jan 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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