-
Instance Variables
-
-
- <% @frame.instance_variables.each do |name, value| %>
- <%= name %> | <%== inspect_value value %> |
- <% end %>
-
+
+
Instance Variables
+
+
+ <% @frame.instance_variables.each do |name, value| %>
+ <%= name %> | <%== inspect_value value %> |
+ <% end %>
+
+
-
-
+
+<% end %>
diff --git a/lib/better_errors/version.rb b/lib/better_errors/version.rb
index 2096403e..1476281a 100644
--- a/lib/better_errors/version.rb
+++ b/lib/better_errors/version.rb
@@ -1,3 +1,3 @@
module BetterErrors
- VERSION = "2.1.1"
+ VERSION = "2.8.3"
end
diff --git a/spec/better_errors/code_formatter_spec.rb b/spec/better_errors/code_formatter_spec.rb
index 93445b46..5b612de6 100644
--- a/spec/better_errors/code_formatter_spec.rb
+++ b/spec/better_errors/code_formatter_spec.rb
@@ -7,13 +7,13 @@ module BetterErrors
let(:formatter) { CodeFormatter.new(filename, 8) }
it "picks an appropriate scanner" do
- formatter.coderay_scanner.should == :ruby
+ expect(formatter.coderay_scanner).to eq(:ruby)
end
it "shows 5 lines of context" do
- formatter.line_range.should == (3..13)
+ expect(formatter.line_range).to eq(3..13)
- formatter.context_lines.should == [
+ expect(formatter.context_lines).to eq([
"three\n",
"four\n",
"five\n",
@@ -25,40 +25,40 @@ module BetterErrors
"eleven\n",
"twelve\n",
"thirteen\n"
- ]
+ ])
end
it "works when the line is right on the edge" do
formatter = CodeFormatter.new(filename, 20)
- formatter.line_range.should == (15..20)
+ expect(formatter.line_range).to eq(15..20)
end
describe CodeFormatter::HTML do
it "highlights the erroring line" do
formatter = CodeFormatter::HTML.new(filename, 8)
- formatter.output.should =~ /highlight.*eight/
+ expect(formatter.output).to match(/highlight.*eight/)
end
it "works when the line is right on the edge" do
formatter = CodeFormatter::HTML.new(filename, 20)
- formatter.output.should_not == formatter.source_unavailable
+ expect(formatter.output).not_to eq(formatter.source_unavailable)
end
it "doesn't barf when the lines don't make any sense" do
formatter = CodeFormatter::HTML.new(filename, 999)
- formatter.output.should == formatter.source_unavailable
+ expect(formatter.output).to eq(formatter.source_unavailable)
end
it "doesn't barf when the file doesn't exist" do
formatter = CodeFormatter::HTML.new("fkdguhskd7e l", 1)
- formatter.output.should == formatter.source_unavailable
+ expect(formatter.output).to eq(formatter.source_unavailable)
end
end
describe CodeFormatter::Text do
it "highlights the erroring line" do
formatter = CodeFormatter::Text.new(filename, 8)
- formatter.output.should == <<-TEXT.gsub(/^ /, "")
+ expect(formatter.output).to eq <<-TEXT.gsub(/^ /, "")
3 three
4 four
5 five
@@ -75,17 +75,17 @@ module BetterErrors
it "works when the line is right on the edge" do
formatter = CodeFormatter::Text.new(filename, 20)
- formatter.output.should_not == formatter.source_unavailable
+ expect(formatter.output).not_to eq(formatter.source_unavailable)
end
it "doesn't barf when the lines don't make any sense" do
formatter = CodeFormatter::Text.new(filename, 999)
- formatter.output.should == formatter.source_unavailable
+ expect(formatter.output).to eq(formatter.source_unavailable)
end
it "doesn't barf when the file doesn't exist" do
formatter = CodeFormatter::Text.new("fkdguhskd7e l", 1)
- formatter.output.should == formatter.source_unavailable
+ expect(formatter.output).to eq(formatter.source_unavailable)
end
end
end
diff --git a/spec/better_errors/error_page_spec.rb b/spec/better_errors/error_page_spec.rb
index 0fc99ccb..74d4f2bb 100644
--- a/spec/better_errors/error_page_spec.rb
+++ b/spec/better_errors/error_page_spec.rb
@@ -1,14 +1,21 @@
require "spec_helper"
+class ErrorPageTestIgnoredClass; end
+
module BetterErrors
describe ErrorPage do
+ # It's necessary to use HTML matchers here that are specific as possible.
+ # This is because if there's an exception within this file, the lines of code will be reflected in the
+ # generated HTML, so any strings being matched against the HTML content will be there if they're within 5
+ # lines of code of the exception that was raised.
+
let!(:exception) { raise ZeroDivisionError, "you divided by zero you silly goose!" rescue $! }
let(:error_page) { ErrorPage.new exception, { "PATH_INFO" => "/some/path" } }
let(:response) { error_page.render }
- let(:empty_binding) {
+ let(:exception_binding) {
local_a = :value_for_local_a
local_b = :value_for_local_b
@@ -19,58 +26,459 @@ module BetterErrors
}
it "includes the error message" do
- response.should include("you divided by zero you silly goose!")
+ expect(response).to have_tag('.exception p', text: /you divided by zero you silly goose!/)
end
it "includes the request path" do
- response.should include("/some/path")
+ expect(response).to have_tag('.exception h2', %r{/some/path})
end
it "includes the exception class" do
- response.should include("ZeroDivisionError")
+ expect(response).to have_tag('.exception h2', /ZeroDivisionError/)
+ end
+
+ context 'when ActiveSupport::ActionableError is available' do
+ before do
+ skip "ActiveSupport missing on this platform" unless Object.constants.include?(:ActiveSupport)
+ skip "ActionableError missing on this platform" unless ActiveSupport.constants.include?(:ActionableError)
+ end
+
+ context 'when ActiveSupport provides one or more actions for this error type' do
+ let(:exception_class) {
+ Class.new(StandardError) do
+ include ActiveSupport::ActionableError
+
+ action "Do a thing" do
+ puts "Did a thing"
+ end
+ end
+ }
+ let(:exception) { exception_binding.eval("raise exception_class") rescue $! }
+
+ it "includes a fix-action form for each action" do
+ expect(response).to have_tag('.fix-actions') do
+ with_tag('form.button_to')
+ with_tag('form.button_to input[type=submit][value="Do a thing"]')
+ end
+ end
+ end
+
+ context 'when ActiveSupport does not provide any actions for this error type' do
+ let(:exception_class) {
+ Class.new(StandardError)
+ }
+ let(:exception) { exception_binding.eval("raise exception_class") rescue $! }
+
+ it "does not include a fix-action form" do
+ expect(response).not_to have_tag('.fix-actions')
+ end
+ end
end
context "variable inspection" do
- let(:exception) { empty_binding.eval("raise") rescue $! }
+ let(:html) { error_page.do_variables("index" => 0)[:html] }
+ let(:exception) { exception_binding.eval("raise") rescue $! }
- if BetterErrors.binding_of_caller_available?
- it "shows local variables" do
- html = error_page.do_variables("index" => 0)[:html]
- html.should include("local_a")
- html.should include(":value_for_local_a")
- html.should include("local_b")
- html.should include(":value_for_local_b")
+ it 'includes an editor link for the full path of the current frame' do
+ expect(html).to have_tag('.location .filename') do
+ with_tag('a[href*="better_errors"]')
end
- else
- it "tells the user to add binding_of_caller to their gemfile to get fancy features" do
- html = error_page.do_variables("index" => 0)[:html]
- html.should include(%{gem "binding_of_caller"})
+ end
+
+ context 'when BETTER_ERRORS_INSIDE_FRAME is set in the environment' do
+ before do
+ ENV['BETTER_ERRORS_INSIDE_FRAME'] = '1'
+ end
+ after do
+ ENV['BETTER_ERRORS_INSIDE_FRAME'] = nil
+ end
+
+ it 'includes an editor link with target=_blank' do
+ expect(html).to have_tag('.location .filename') do
+ with_tag('a[href*="better_errors"][target="_blank"]')
+ end
end
end
- it "shows instance variables" do
- html = error_page.do_variables("index" => 0)[:html]
- html.should include("inst_c")
- html.should include(":value_for_inst_c")
- html.should include("inst_d")
- html.should include(":value_for_inst_d")
+ context 'when BETTER_ERRORS_INSIDE_FRAME is not set in the environment' do
+ it 'includes an editor link without target=_blank' do
+ expect(html).to have_tag('.location .filename') do
+ with_tag('a[href*="better_errors"]:not([target="_blank"])')
+ end
+ end
end
- it "shows filter instance variables" do
- BetterErrors.stub(:ignored_instance_variables).and_return([ :@inst_d ])
- html = error_page.do_variables("index" => 0)[:html]
- html.should include("inst_c")
- html.should include(":value_for_inst_c")
- html.should_not include('
@inst_d | ')
- html.should_not include("
:value_for_inst_d
")
+ context "when binding_of_caller is loaded" do
+ before do
+ skip "binding_of_caller is not loaded" unless BetterErrors.binding_of_caller_available?
+ end
+
+ it "shows local variables" do
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: 'local_a')
+ with_tag('pre', text: ':value_for_local_a')
+ end
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: 'local_b')
+ with_tag('pre', text: ':value_for_local_b')
+ end
+ end
+
+ it "shows instance variables" do
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@inst_c')
+ with_tag('pre', text: ':value_for_inst_c')
+ end
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@inst_d')
+ with_tag('pre', text: ':value_for_inst_d')
+ end
+ end
+
+ context 'when ignored_classes includes the class name of a local variable' do
+ before do
+ allow(BetterErrors).to receive(:ignored_classes).and_return(['ErrorPageTestIgnoredClass'])
+ end
+
+ let(:exception_binding) {
+ local_a = :value_for_local_a
+ local_b = ErrorPageTestIgnoredClass.new
+
+ @inst_c = :value_for_inst_c
+ @inst_d = ErrorPageTestIgnoredClass.new
+
+ binding
+ }
+
+ it "does not include that value" do
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: 'local_a')
+ with_tag('pre', text: ':value_for_local_a')
+ end
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: 'local_b')
+ with_tag('.unsupported', text: /Instance of ignored class/)
+ with_tag('.unsupported', text: /BetterErrors\.ignored_classes/)
+ end
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@inst_c')
+ with_tag('pre', text: ':value_for_inst_c')
+ end
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@inst_d')
+ with_tag('.unsupported', text: /Instance of ignored class/)
+ with_tag('.unsupported', text: /BetterErrors\.ignored_classes/)
+ end
+ end
+ end
+
+ it "does not show filtered variables" do
+ allow(BetterErrors).to receive(:ignored_instance_variables).and_return([:@inst_d])
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@inst_c')
+ with_tag('pre', text: ':value_for_inst_c')
+ end
+ expect(html).not_to have_tag('div.variables td.name', text: '@inst_d')
+ end
+
+ context 'when maximum_variable_inspect_size is set' do
+ before do
+ BetterErrors.maximum_variable_inspect_size = 1010
+ end
+
+ context 'on a platform with ObjectSpace' do
+ before do
+ skip "Missing on this platform" unless Object.constants.include?(:ObjectSpace)
+ end
+
+ context 'with a variable that is smaller than maximum_variable_inspect_size' do
+ let(:exception_binding) {
+ @small = content
+
+ binding
+ }
+ let(:content) { 'A' * 480 }
+
+ it "shows the variable content" do
+ expect(html).to have_tag('div.variables', text: %r{#{content}})
+ end
+ end
+
+ context 'with a variable that is larger than maximum_variable_inspect_size' do
+ context 'but has an #inspect that returns a smaller value' do
+ let(:exception_binding) {
+ @big = content
+
+ binding
+ }
+ let(:content) {
+ class ExtremelyLargeInspectableTestValue
+ def initialize
+ @a = 'A' * 1101
+ end
+ def inspect
+ "shortval"
+ end
+ end
+ ExtremelyLargeInspectableTestValue.new
+ }
+
+ it "shows the variable content" do
+ expect(html).to have_tag('div.variables', text: /shortval/)
+ end
+ end
+ context 'and does not implement #inspect' do
+ let(:exception_binding) {
+ @big = content
+
+ binding
+ }
+ let(:content) { 'A' * 1101 }
+
+ it "includes an indication that the variable was too large" do
+ expect(html).not_to have_tag('div.variables', text: %r{#{content}})
+ expect(html).to have_tag('div.variables', text: /Object too large/)
+ end
+ end
+
+ context "when the variable's class is anonymous" do
+ let(:exception_binding) {
+ @big_anonymous = Class.new do
+ def initialize
+ @content = 'A' * 1101
+ end
+ end.new
+
+ binding
+ }
+
+ it "does not attempt to show the class name" do
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@big_anonymous')
+ with_tag('.unsupported', text: /Object too large/)
+ with_tag('.unsupported', text: /Adjust BetterErrors.maximum_variable_inspect_size/)
+ end
+ end
+ end
+ end
+ end
+
+ context 'on a platform without ObjectSpace' do
+ before do
+ Object.send(:remove_const, :ObjectSpace) if Object.constants.include?(:ObjectSpace)
+ end
+ after do
+ require "objspace" rescue nil
+ end
+
+ context 'with a variable that is smaller than maximum_variable_inspect_size' do
+ let(:exception_binding) {
+ @small = content
+
+ binding
+ }
+ let(:content) { 'A' * 480 }
+
+ it "shows the variable content" do
+ expect(html).to have_tag('div.variables', text: %r{#{content}})
+ end
+ end
+
+ context 'with a variable that is larger than maximum_variable_inspect_size' do
+ context 'but has an #inspect that returns a smaller value' do
+ let(:exception_binding) {
+ @big = content
+
+ binding
+ }
+ let(:content) {
+ class ExtremelyLargeInspectableTestValue
+ def initialize
+ @a = 'A' * 1101
+ end
+ def inspect
+ "shortval"
+ end
+ end
+ ExtremelyLargeInspectableTestValue.new
+ }
+
+ it "shows the variable content" do
+ expect(html).to have_tag('div.variables', text: /shortval/)
+ end
+ end
+ context 'and does not implement #inspect' do
+ let(:exception_binding) {
+ @big = content
+
+ binding
+ }
+ let(:content) { 'A' * 1101 }
+
+ it "includes an indication that the variable was too large" do
+
+ expect(html).not_to have_tag('div.variables', text: %r{#{content}})
+ expect(html).to have_tag('div.variables', text: /Object too large/)
+ end
+ end
+ end
+
+ context "when the variable's class is anonymous" do
+ let(:exception_binding) {
+ @big_anonymous = Class.new do
+ def initialize
+ @content = 'A' * 1101
+ end
+ end.new
+
+ binding
+ }
+
+ it "does not attempt to show the class name" do
+ expect(html).to have_tag('div.variables tr') do
+ with_tag('td.name', text: '@big_anonymous')
+ with_tag('.unsupported', text: /Object too large/)
+ with_tag('.unsupported', text: /Adjust BetterErrors.maximum_variable_inspect_size/)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when maximum_variable_inspect_size is disabled' do
+ before do
+ BetterErrors.maximum_variable_inspect_size = nil
+ end
+
+ let(:exception_binding) {
+ @big = content
+
+ binding
+ }
+ let(:content) { 'A' * 100_001 }
+
+ it "includes the content of large variables" do
+ expect(html).to have_tag('div.variables', text: %r{#{content}})
+ expect(html).not_to have_tag('div.variables', text: /Object too large/)
+ end
+ end
+ end
+
+ context "when binding_of_caller is not loaded" do
+ before do
+ skip "binding_of_caller is loaded" if BetterErrors.binding_of_caller_available?
+ end
+
+ it "tells the user to add binding_of_caller to their gemfile to get fancy features" do
+ expect(html).not_to have_tag('div.variables', text: /gem "binding_of_caller"/)
+ end
end
end
it "doesn't die if the source file is not a real filename" do
- exception.stub(:backtrace).and_return([
+ allow(exception).to receive(:__better_errors_bindings_stack).and_return([])
+ allow(exception).to receive(:backtrace).and_return([
"
:10:in `spawn_rack_application'"
])
- response.should include("Source unavailable")
+ expect(response).to have_tag('.frames li .location .filename', text: '')
+ end
+
+ context 'with an exception with blank lines' do
+ class SpacedError < StandardError
+ def initialize(message = nil)
+ message = "\n\n#{message}" if message
+ super
+ end
+ end
+
+ let!(:exception) { raise SpacedError, "Danger Warning!" rescue $! }
+
+ it 'does not include leading blank lines in exception_message' do
+ expect(exception.message).to match(/\A\n\n/)
+ expect(error_page.exception_message).not_to match(/\A\n\n/)
+ end
+ end
+
+ describe '#do_eval' do
+ let(:exception) { exception_binding.eval("raise") rescue $! }
+ subject(:do_eval) { error_page.do_eval("index" => 0, "source" => command) }
+ let(:command) { 'EvalTester.stuff_was_done(:yep)' }
+ before do
+ stub_const('EvalTester', eval_tester)
+ end
+ let(:eval_tester) { double('EvalTester', stuff_was_done: 'response') }
+
+ context 'without binding_of_caller' do
+ before do
+ skip("Disabled with binding_of_caller") if defined? ::BindingOfCaller
+ end
+
+ it "does not evaluate the code" do
+ do_eval
+ expect(eval_tester).to_not have_received(:stuff_was_done).with(:yep)
+ end
+
+ it 'returns an error indicating no REPL' do
+ expect(do_eval).to include(
+ error: "REPL unavailable in this stack frame",
+ )
+ end
+ end
+
+ context 'with binding_of_caller available' do
+ before do
+ skip("Disabled without binding_of_caller") unless defined? ::BindingOfCaller
+ end
+
+ context 'with Pry disabled or unavailable' do
+ it "evaluates the code" do
+ do_eval
+ expect(eval_tester).to have_received(:stuff_was_done).with(:yep)
+ end
+
+ it 'returns a hash of the code and its result' do
+ expect(do_eval).to include(
+ highlighted_input: /stuff_was_done/,
+ prefilled_input: '',
+ prompt: '>>',
+ result: "=> \"response\"\n",
+ )
+ end
+ end
+
+ context 'with Pry enabled' do
+ before do
+ skip("Disabled without pry") unless defined? ::Pry
+
+ BetterErrors.use_pry!
+ # Cause the provider to be unselected, so that it will be re-detected.
+ BetterErrors::REPL.provider = nil
+ end
+ after do
+ BetterErrors::REPL::PROVIDERS.shift
+ BetterErrors::REPL.provider = nil
+
+ # Ensure the Pry REPL file has not been included. If this is not done,
+ # the constant leaks into other examples.
+ BetterErrors::REPL.send(:remove_const, :Pry)
+ end
+
+ it "evaluates the code" do
+ BetterErrors::REPL.provider
+ do_eval
+ expect(eval_tester).to have_received(:stuff_was_done).with(:yep)
+ end
+
+ it 'returns a hash of the code and its result' do
+ expect(do_eval).to include(
+ highlighted_input: /stuff_was_done/,
+ prefilled_input: '',
+ prompt: '>>',
+ result: "=> \"response\"\n",
+ )
+ end
+ end
+ end
end
end
end
diff --git a/spec/better_errors/middleware_spec.rb b/spec/better_errors/middleware_spec.rb
index 4cdd7826..0cdd8216 100644
--- a/spec/better_errors/middleware_spec.rb
+++ b/spec/better_errors/middleware_spec.rb
@@ -4,44 +4,50 @@ module BetterErrors
describe Middleware do
let(:app) { Middleware.new(->env { ":)" }) }
let(:exception) { RuntimeError.new("oh no :(") }
+ let(:status) { response_env[0] }
+ let(:headers) { response_env[1] }
+ let(:body) { response_env[2].join }
- it "passes non-error responses through" do
- app.call({}).should == ":)"
+ context 'when the application raises no exception' do
+ it "passes non-error responses through" do
+ expect(app.call({})).to eq(":)")
+ end
end
it "calls the internal methods" do
- app.should_receive :internal_call
+ expect(app).to receive :internal_call
app.call("PATH_INFO" => "/__better_errors/1/preform_awesomness")
end
it "calls the internal methods on any subfolder path" do
- app.should_receive :internal_call
+ expect(app).to receive :internal_call
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/1/preform_awesomness")
end
it "shows the error page" do
- app.should_receive :show_error_page
+ expect(app).to receive :show_error_page
app.call("PATH_INFO" => "/__better_errors/")
end
- it "shows the error page on any subfolder path" do
- app.should_receive :show_error_page
- app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
- end
-
it "doesn't show the error page to a non-local address" do
- app.should_not_receive :better_errors_call
+ expect(app).not_to receive :better_errors_call
app.call("REMOTE_ADDR" => "1.2.3.4")
end
it "shows to a whitelisted IP" do
BetterErrors::Middleware.allow_ip! '77.55.33.11'
- app.should_receive :better_errors_call
+ expect(app).to receive :better_errors_call
+ app.call("REMOTE_ADDR" => "77.55.33.11")
+ end
+
+ it "shows to a whitelisted IPAddr" do
+ BetterErrors::Middleware.allow_ip! IPAddr.new('77.55.33.0/24')
+ expect(app).to receive :better_errors_call
app.call("REMOTE_ADDR" => "77.55.33.11")
end
it "respects the X-Forwarded-For header" do
- app.should_not_receive :better_errors_call
+ expect(app).not_to receive :better_errors_call
app.call(
"REMOTE_ADDR" => "127.0.0.1",
"HTTP_X_FORWARDED_FOR" => "1.2.3.4",
@@ -56,30 +62,97 @@ module BetterErrors
expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error
end
- context "when requesting the /__better_errors manually" do
- let(:app) { Middleware.new(->env { ":)" }) }
+ context "when /__better_errors is requested directly" do
+ let(:response_env) { app.call("PATH_INFO" => "/__better_errors") }
+
+ context "when no error has been recorded since startup" do
+ it "shows that no errors have been recorded" do
+ expect(body).to match /No errors have been recorded yet./
+ end
- it "shows that no errors have been recorded" do
- status, headers, body = app.call("PATH_INFO" => "/__better_errors")
- body.join.should match /No errors have been recorded yet./
+ it 'does not attempt to use ActionDispatch::ExceptionWrapper on the nil exception' do
+ ad_ew = double("ActionDispatch::ExceptionWrapper")
+ stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
+ expect(ad_ew).to_not receive :new
+
+ response_env
+ end
+
+ context 'when requested inside a subfolder path' do
+ let(:response_env) { app.call("PATH_INFO" => "/any_sub/folder/__better_errors") }
+
+ it "shows that no errors have been recorded" do
+ expect(body).to match /No errors have been recorded yet./
+ end
+ end
end
- it "shows that no errors have been recorded on any subfolder path" do
- status, headers, body = app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors")
- body.join.should match /No errors have been recorded yet./
+ context 'when an error has been recorded' do
+ let(:app) {
+ Middleware.new(->env do
+ # Only raise on the first request
+ raise exception unless @already_raised
+ @already_raised = true
+ end)
+ }
+ before do
+ app.call({})
+ end
+
+ it 'returns the information of the most recent error' do
+ expect(body).to include("oh no :(")
+ end
+
+ it 'does not attempt to use ActionDispatch::ExceptionWrapper' do
+ ad_ew = double("ActionDispatch::ExceptionWrapper")
+ stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
+ expect(ad_ew).to_not receive :new
+
+ response_env
+ end
+
+ context 'when inside a subfolder path' do
+ let(:response_env) { app.call("PATH_INFO" => "/any_sub/folder/__better_errors") }
+
+ it "shows the error page on any subfolder path" do
+ expect(app).to receive :show_error_page
+ app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
+ end
+ end
end
end
context "when handling an error" do
let(:app) { Middleware.new(->env { raise exception }) }
+ let(:response_env) { app.call({}) }
it "returns status 500" do
- status, headers, body = app.call({})
+ expect(status).to eq(500)
+ end
+
+ context "when the exception has a cause" do
+ before do
+ pending "This Ruby does not support `cause`" unless Exception.new.respond_to?(:cause)
+ end
+
+ let(:app) {
+ Middleware.new(->env {
+ begin
+ raise "First Exception"
+ rescue
+ raise "Second Exception"
+ end
+ })
+ }
- status.should == 500
+ it "shows the exception as-is" do
+ expect(status).to eq(500)
+ expect(body).to match(/\nSecond Exception\n/)
+ expect(body).not_to match(/\nFirst Exception\n/)
+ end
end
- context "original_exception" do
+ context "when the exception responds to #original_exception" do
class OriginalExceptionException < Exception
attr_reader :original_exception
@@ -89,65 +162,394 @@ def initialize(message, original_exception = nil)
end
end
- it "shows Original Exception if it responds_to and has an original_exception" do
- app = Middleware.new(->env {
- raise OriginalExceptionException.new("Other Exception", Exception.new("Original Exception"))
- })
-
- status, _, body = app.call({})
-
- status.should == 500
- body.join.should_not match(/Other Exception/)
- body.join.should match(/Original Exception/)
+ context 'and has one' do
+ let(:app) {
+ Middleware.new(->env {
+ raise OriginalExceptionException.new("Second Exception", Exception.new("First Exception"))
+ })
+ }
+
+ it "shows the original exception instead of the last-raised one" do
+ expect(status).to eq(500)
+ expect(body).not_to match(/Second Exception/)
+ expect(body).to match(/First Exception/)
+ end
end
- it "won't crash if the exception responds_to but doesn't have an original_exception" do
- app = Middleware.new(->env {
- raise OriginalExceptionException.new("Other Exception")
- })
-
- status, _, body = app.call({})
+ context 'and does not have one' do
+ let(:app) {
+ Middleware.new(->env {
+ raise OriginalExceptionException.new("The Exception")
+ })
+ }
- status.should == 500
- body.join.should match(/Other Exception/)
+ it "shows the exception as-is" do
+ expect(status).to eq(500)
+ expect(body).to match(/The Exception/)
+ end
end
end
it "returns ExceptionWrapper's status_code" do
ad_ew = double("ActionDispatch::ExceptionWrapper")
- ad_ew.stub('new').with({}, exception ){ double("ExceptionWrapper", status_code: 404) }
+ allow(ad_ew).to receive('new').with(anything, exception) { double("ExceptionWrapper", status_code: 404) }
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
- status, headers, body = app.call({})
-
- status.should == 404
+ expect(status).to eq(404)
end
it "returns UTF-8 error pages" do
- status, headers, body = app.call({})
+ expect(headers["Content-Type"]).to match /charset=utf-8/
+ end
+
+ it "returns text content by default" do
+ expect(headers["Content-Type"]).to match /text\/plain/
+ end
+
+ context 'when a CSRF token cookie is not specified' do
+ it 'includes a newly-generated CSRF token cookie' do
+ expect(headers).to include(
+ 'Set-Cookie' => /BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=[-a-z0-9]+; path=\/; HttpOnly; SameSite=Strict/,
+ )
+ end
+ end
+
+ context 'when a CSRF token cookie is specified' do
+ let(:response_env) { app.call({ 'HTTP_COOKIE' => "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=abc123" }) }
+
+ it 'does not set a new CSRF token cookie' do
+ expect(headers).not_to include('Set-Cookie')
+ end
+ end
+
+ context 'when the Accept header specifies HTML first' do
+ let(:response_env) { app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*") }
+
+ it "returns HTML content" do
+ expect(headers["Content-Type"]).to match /text\/html/
+ end
+
+ it 'includes the newly-generated CSRF token in the body of the page' do
+ matches = headers['Set-Cookie'].match(/BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=(?[-a-z0-9]+); path=\/; HttpOnly; SameSite=Strict/)
+ expect(body).to include(matches[:tok])
+ end
+
+ context 'when a CSRF token cookie is specified' do
+ let(:response_env) {
+ app.call({
+ 'HTTP_COOKIE' => "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfTokenGHI",
+ "HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*",
+ })
+ }
+
+ it 'includes that CSRF token in the body of the page' do
+ expect(body).to include('csrfTokenGHI')
+ end
+ end
+ end
- headers["Content-Type"].should match /charset=utf-8/
+ context 'the logger' do
+ let(:logger) { double('logger', fatal: nil) }
+ before do
+ allow(BetterErrors).to receive(:logger).and_return(logger)
+ end
+
+ it "receives the exception as a fatal message" do
+ expect(logger).to receive(:fatal).with(/RuntimeError/)
+ response_env
+ end
+
+ context 'when Rails is being used' do
+ before do
+ skip("Rails not included in this run") unless defined? Rails
+ end
+
+ it "receives the exception without filtered backtrace frames" do
+ expect(logger).to receive(:fatal) do |message|
+ expect(message).to_not match(/rspec-core/)
+ end
+ response_env
+ end
+ end
+ context 'when Rails is not being used' do
+ before do
+ skip("Rails is included in this run") if defined? Rails
+ end
+
+ it "receives the exception with all backtrace frames" do
+ expect(logger).to receive(:fatal) do |message|
+ expect(message).to match(/rspec-core/)
+ end
+ response_env
+ end
+ end
end
+ end
+
+ context "requesting the variables for a specific frame" do
+ let(:env) { {} }
+ let(:response_env) {
+ app.call(request_env)
+ }
+ let(:request_env) {
+ Rack::MockRequest.env_for("/__better_errors/#{id}/variables", input: StringIO.new(JSON.dump(request_body_data)))
+ }
+ let(:request_body_data) { { "index" => 0 } }
+ let(:json_body) { JSON.parse(body) }
+ let(:id) { 'abcdefg' }
+
+ context 'when no errors have been recorded' do
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'No exception information available',
+ 'explanation' => /application has been restarted/,
+ )
+ end
+
+ context 'when Middleman is in use' do
+ let!(:middleman) { class_double("Middleman").as_stubbed_const }
+ it 'returns a JSON error' do
+ expect(json_body['explanation'])
+ .to match(/Middleman reloads all dependencies/)
+ end
+ end
+
+ context 'when Shotgun is in use' do
+ let!(:shotgun) { class_double("Shotgun").as_stubbed_const }
+
+ it 'returns a JSON error' do
+ expect(json_body['explanation'])
+ .to match(/The shotgun gem/)
+ end
+
+ context 'when Hanami is also in use' do
+ let!(:hanami) { class_double("Hanami").as_stubbed_const }
+ it 'returns a JSON error' do
+ expect(json_body['explanation'])
+ .to match(/--no-code-reloading/)
+ end
+ end
+ end
+ end
+
+ context 'when an error has been recorded' do
+ let(:error_page) { ErrorPage.new(exception, env) }
+ before do
+ app.instance_variable_set('@error_page', error_page)
+ end
+
+ context 'but it does not match the request' do
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Session expired',
+ 'explanation' => /no longer available in memory/,
+ )
+ end
+ end
+
+ context 'and its ID matches the requested ID' do
+ let(:id) { error_page.id }
+
+ context 'when the body csrfToken matches the CSRF token cookie' do
+ let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
+ before do
+ request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken123"
+ end
+
+ context 'when the Content-Type of the request is application/json' do
+ before do
+ request_env['CONTENT_TYPE'] = 'application/json'
+ end
+
+ it 'returns JSON containing the HTML content' do
+ expect(error_page).to receive(:do_variables).and_return(html: "")
+ expect(json_body).to match(
+ 'html' => '',
+ )
+ end
+ end
+
+ context 'when the Content-Type of the request is application/json' do
+ before do
+ request_env['HTTP_CONTENT_TYPE'] = 'application/json'
+ end
+
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Request not acceptable',
+ 'explanation' => /did not match an acceptable content type/,
+ )
+ end
+ end
+ end
- it "returns text pages by default" do
- status, headers, body = app.call({})
+ context 'when the body csrfToken does not match the CSRF token cookie' do
+ let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
+ before do
+ request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken456"
+ end
+
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Invalid CSRF Token',
+ 'explanation' => /session might have been cleared/,
+ )
+ end
+ end
- headers["Content-Type"].should match /text\/plain/
+ context 'when there is no CSRF token in the request' do
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Invalid CSRF Token',
+ 'explanation' => /session might have been cleared/,
+ )
+ end
+ end
+ end
end
+ end
- it "returns HTML pages by default" do
- # Chrome's 'Accept' header looks similar this.
- status, headers, body = app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*")
+ context "requesting eval for a specific frame" do
+ let(:env) { {} }
+ let(:response_env) {
+ app.call(request_env)
+ }
+ let(:request_env) {
+ Rack::MockRequest.env_for("/__better_errors/#{id}/eval", input: StringIO.new(JSON.dump(request_body_data)))
+ }
+ let(:request_body_data) { { "index" => 0, source: "do_a_thing" } }
+ let(:json_body) { JSON.parse(body) }
+ let(:id) { 'abcdefg' }
+
+ context 'when no errors have been recorded' do
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'No exception information available',
+ 'explanation' => /application has been restarted/,
+ )
+ end
- headers["Content-Type"].should match /text\/html/
+ context 'when Middleman is in use' do
+ let!(:middleman) { class_double("Middleman").as_stubbed_const }
+ it 'returns a JSON error' do
+ expect(json_body['explanation'])
+ .to match(/Middleman reloads all dependencies/)
+ end
+ end
+
+ context 'when Shotgun is in use' do
+ let!(:shotgun) { class_double("Shotgun").as_stubbed_const }
+
+ it 'returns a JSON error' do
+ expect(json_body['explanation'])
+ .to match(/The shotgun gem/)
+ end
+
+ context 'when Hanami is also in use' do
+ let!(:hanami) { class_double("Hanami").as_stubbed_const }
+ it 'returns a JSON error' do
+ expect(json_body['explanation'])
+ .to match(/--no-code-reloading/)
+ end
+ end
+ end
end
- it "logs the exception" do
- logger = Object.new
- logger.should_receive :fatal
- BetterErrors.stub(:logger).and_return(logger)
+ context 'when an error has been recorded' do
+ let(:error_page) { ErrorPage.new(exception, env) }
+ before do
+ app.instance_variable_set('@error_page', error_page)
+ end
+
+ context 'but it does not match the request' do
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Session expired',
+ 'explanation' => /no longer available in memory/,
+ )
+ end
+ end
+
+ context 'and its ID matches the requested ID' do
+ let(:id) { error_page.id }
+
+ context 'when the body csrfToken matches the CSRF token cookie' do
+ let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
+ before do
+ request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken123"
+ end
+
+ context 'when the Content-Type of the request is application/json' do
+ before do
+ request_env['CONTENT_TYPE'] = 'application/json'
+ end
+
+ it 'returns JSON containing the eval result' do
+ expect(error_page).to receive(:do_eval).and_return(prompt: '#', result: "much_stuff_here")
+ expect(json_body).to match(
+ 'prompt' => '#',
+ 'result' => 'much_stuff_here',
+ )
+ end
+ end
+
+ context 'when the Content-Type of the request is application/json' do
+ before do
+ request_env['HTTP_CONTENT_TYPE'] = 'application/json'
+ end
+
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Request not acceptable',
+ 'explanation' => /did not match an acceptable content type/,
+ )
+ end
+ end
+ end
+
+ context 'when the body csrfToken does not match the CSRF token cookie' do
+ let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
+ before do
+ request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken456"
+ end
+
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Invalid CSRF Token',
+ 'explanation' => /session might have been cleared/,
+ )
+ end
+ end
+
+ context 'when there is no CSRF token in the request' do
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Invalid CSRF Token',
+ 'explanation' => /session might have been cleared/,
+ )
+ end
+ end
+ end
+ end
+ end
- app.call({})
+ context "requesting an invalid internal method" do
+ let(:env) { {} }
+ let(:response_env) {
+ app.call(request_env)
+ }
+ let(:request_env) {
+ Rack::MockRequest.env_for("/__better_errors/#{id}/invalid", input: StringIO.new(JSON.dump(request_body_data)))
+ }
+ let(:request_body_data) { { "index" => 0 } }
+ let(:json_body) { JSON.parse(body) }
+ let(:id) { 'abcdefg' }
+
+ it 'returns a JSON error' do
+ expect(json_body).to match(
+ 'error' => 'Not found',
+ 'explanation' => /recognized internal call/,
+ )
end
end
end
diff --git a/spec/better_errors/raised_exception_spec.rb b/spec/better_errors/raised_exception_spec.rb
index 6ffe86b1..45fa730d 100644
--- a/spec/better_errors/raised_exception_spec.rb
+++ b/spec/better_errors/raised_exception_spec.rb
@@ -1,31 +1,52 @@
require "spec_helper"
+require "rspec/its"
module BetterErrors
describe RaisedException do
let(:exception) { RuntimeError.new("whoops") }
subject { RaisedException.new(exception) }
- its(:exception) { should == exception }
- its(:message) { should == "whoops" }
- its(:type) { should == RuntimeError }
+ its(:exception) { is_expected.to eq exception }
+ its(:message) { is_expected.to eq "whoops" }
+ its(:type) { is_expected.to eq RuntimeError }
- context "when the exception wraps another exception" do
+ context 'when the exception is an ActionView::Template::Error that responds to #cause (Rails 6+)' do
+ before do
+ stub_const(
+ "ActionView::Template::Error",
+ Class.new(StandardError) do
+ def cause
+ RuntimeError.new("something went wrong!")
+ end
+ end
+ )
+ end
+ let(:exception) {
+ ActionView::Template::Error.new("undefined method `something!' for #")
+ }
+
+ its(:message) { is_expected.to eq "something went wrong!" }
+ its(:type) { is_expected.to eq RuntimeError }
+ end
+
+ context 'when the exception is a Rails < 6 exception that has an #original_exception' do
let(:original_exception) { RuntimeError.new("something went wrong!") }
let(:exception) { double(:original_exception => original_exception) }
- its(:exception) { should == original_exception }
- its(:message) { should == "something went wrong!" }
+ its(:exception) { is_expected.to eq original_exception }
+ its(:message) { is_expected.to eq "something went wrong!" }
+ its(:type) { is_expected.to eq RuntimeError }
end
- context "when the exception is a syntax error" do
+ context "when the exception is a SyntaxError" do
let(:exception) { SyntaxError.new("foo.rb:123: you made a typo!") }
- its(:message) { should == "you made a typo!" }
- its(:type) { should == SyntaxError }
+ its(:message) { is_expected.to eq "you made a typo!" }
+ its(:type) { is_expected.to eq SyntaxError }
it "has the right filename and line number in the backtrace" do
- subject.backtrace.first.filename.should == "foo.rb"
- subject.backtrace.first.line.should == 123
+ expect(subject.backtrace.first.filename).to eq("foo.rb")
+ expect(subject.backtrace.first.line).to eq(123)
end
end
@@ -40,15 +61,29 @@ module BetterErrors
end
}
- its(:message) { should == "you made a typo!" }
- its(:type) { should == Haml::SyntaxError }
+ its(:message) { is_expected.to eq "you made a typo!" }
+ its(:type) { is_expected.to eq Haml::SyntaxError }
it "has the right filename and line number in the backtrace" do
- subject.backtrace.first.filename.should == "foo.rb"
- subject.backtrace.first.line.should == 123
+ expect(subject.backtrace.first.filename).to eq("foo.rb")
+ expect(subject.backtrace.first.line).to eq(123)
end
end
+ # context "when the exception is an ActionView::Template::Error" do
+ #
+ # let(:exception) {
+ # ActionView::Template::Error.new("undefined method `something!' for #")
+ # }
+ #
+ # its(:message) { is_expected.to eq "undefined method `something!' for #" }
+ #
+ # it "has the right filename and line number in the backtrace" do
+ # expect(subject.backtrace.first.filename).to eq("app/views/foo/bar.haml")
+ # expect(subject.backtrace.first.line).to eq(42)
+ # end
+ # end
+ #
context "when the exception is a Coffeelint syntax error" do
before do
stub_const("Sprockets::Coffeelint::Error", Class.new(SyntaxError))
@@ -60,12 +95,12 @@ module BetterErrors
end
}
- its(:message) { should == "[stdin]:11:88: error: unexpected=" }
- its(:type) { should == Sprockets::Coffeelint::Error }
+ its(:message) { is_expected.to eq "[stdin]:11:88: error: unexpected=" }
+ its(:type) { is_expected.to eq Sprockets::Coffeelint::Error }
it "has the right filename and line number in the backtrace" do
- subject.backtrace.first.filename.should == "app/assets/javascripts/files/index.coffee"
- subject.backtrace.first.line.should == 11
+ expect(subject.backtrace.first.filename).to eq("app/assets/javascripts/files/index.coffee")
+ expect(subject.backtrace.first.line).to eq(11)
end
end
diff --git a/spec/better_errors/repl/basic_spec.rb b/spec/better_errors/repl/basic_spec.rb
index cc71a955..1db8b4c9 100644
--- a/spec/better_errors/repl/basic_spec.rb
+++ b/spec/better_errors/repl/basic_spec.rb
@@ -10,7 +10,9 @@ module REPL
binding
}
- let(:repl) { Basic.new fresh_binding }
+ let!(:exception) { raise ZeroDivisionError, "you divided by zero you silly goose!" rescue $! }
+
+ let(:repl) { Basic.new(fresh_binding, exception) }
it_behaves_like "a REPL provider"
end
diff --git a/spec/better_errors/repl/pry_spec.rb b/spec/better_errors/repl/pry_spec.rb
index 1aa502c5..984f03b5 100644
--- a/spec/better_errors/repl/pry_spec.rb
+++ b/spec/better_errors/repl/pry_spec.rb
@@ -1,40 +1,51 @@
require "spec_helper"
-require "pry"
-require "better_errors/repl/pry"
require "better_errors/repl/shared_examples"
-module BetterErrors
- module REPL
- describe Pry do
- let(:fresh_binding) {
- local_a = 123
- binding
- }
-
- let(:repl) { Pry.new fresh_binding }
-
- it "does line continuation" do
- output, prompt, filled = repl.send_input ""
- output.should == "=> nil\n"
- prompt.should == ">>"
- filled.should == ""
-
- output, prompt, filled = repl.send_input "def f(x)"
- output.should == ""
- prompt.should == ".."
- filled.should == " "
-
- output, prompt, filled = repl.send_input "end"
- if RUBY_VERSION >= "2.1.0"
- output.should == "=> :f\n"
- else
- output.should == "=> nil\n"
- end
- prompt.should == ">>"
- filled.should == ""
- end
+if defined? ::Pry
+ RSpec.describe 'BetterErrors::REPL::Pry' do
+ before(:all) do
+ load "better_errors/repl/pry.rb"
+ end
+ after(:all) do
+ # Ensure the Pry REPL file has not been included. If this is not done,
+ # the constant leaks into other examples.
+ # In practice, this constant is only defined if `use_pry!` is called and then the
+ # REPL is used, causing BetterErrors::REPL to require the file.
+ BetterErrors::REPL.send(:remove_const, :Pry)
+ end
+
+ let(:fresh_binding) {
+ local_a = 123
+ binding
+ }
+
+ let!(:exception) { raise ZeroDivisionError, "you divided by zero you silly goose!" rescue $! }
- it_behaves_like "a REPL provider"
+ let(:repl) { BetterErrors::REPL::Pry.new(fresh_binding, exception) }
+
+ it "does line continuation", :aggregate_failures do
+ output, prompt, filled = repl.send_input ""
+ expect(output).to eq("=> nil\n")
+ expect(prompt).to eq(">>")
+ expect(filled).to eq("")
+
+ output, prompt, filled = repl.send_input "def f(x)"
+ expect(output).to eq("")
+ expect(prompt).to eq("..")
+ expect(filled).to eq(" ")
+
+ output, prompt, filled = repl.send_input "end"
+ if RUBY_VERSION >= "2.1.0"
+ expect(output).to eq("=> :f\n")
+ else
+ expect(output).to eq("=> nil\n")
+ end
+ expect(prompt).to eq(">>")
+ expect(filled).to eq("")
end
+
+ it_behaves_like "a REPL provider"
end
+else
+ puts "Skipping Pry specs because pry is not in the bundle"
end
diff --git a/spec/better_errors/repl/shared_examples.rb b/spec/better_errors/repl/shared_examples.rb
index 4925d8f1..296c9f29 100644
--- a/spec/better_errors/repl/shared_examples.rb
+++ b/spec/better_errors/repl/shared_examples.rb
@@ -1,18 +1,18 @@
shared_examples_for "a REPL provider" do
it "evaluates ruby code in a given context" do
repl.send_input("local_a = 456")
- fresh_binding.eval("local_a").should == 456
+ expect(fresh_binding.eval("local_a")).to eq(456)
end
it "returns a tuple of output and the new prompt" do
output, prompt = repl.send_input("1 + 2")
- output.should == "=> 3\n"
- prompt.should == ">>"
+ expect(output).to eq("=> 3\n")
+ expect(prompt).to eq(">>")
end
it "doesn't barf if the code throws an exception" do
output, prompt = repl.send_input("raise Exception")
- output.should include "Exception: Exception"
- prompt.should == ">>"
+ expect(output).to include "Exception: Exception"
+ expect(prompt).to eq(">>")
end
end
diff --git a/spec/better_errors/stack_frame_spec.rb b/spec/better_errors/stack_frame_spec.rb
index 3d9505c0..df96c90a 100644
--- a/spec/better_errors/stack_frame_spec.rb
+++ b/spec/better_errors/stack_frame_spec.rb
@@ -4,80 +4,100 @@ module BetterErrors
describe StackFrame do
context "#application?" do
it "is true for application filenames" do
- BetterErrors.stub(:application_root).and_return("/abc/xyz")
+ allow(BetterErrors).to receive(:application_root).and_return("/abc/xyz")
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
- frame.should be_application
+ expect(frame).to be_application
end
it "is false for everything else" do
- BetterErrors.stub(:application_root).and_return("/abc/xyz")
+ allow(BetterErrors).to receive(:application_root).and_return("/abc/xyz")
frame = StackFrame.new("/abc/nope", 123, "foo")
- frame.should_not be_application
+ expect(frame).not_to be_application
end
it "doesn't care if no application_root is set" do
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
- frame.should_not be_application
+ expect(frame).not_to be_application
end
end
context "#gem?" do
it "is true for gem filenames" do
- Gem.stub(:path).and_return(["/abc/xyz"])
+ allow(Gem).to receive(:path).and_return(["/abc/xyz"])
frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
- frame.should be_gem
+ expect(frame).to be_gem
end
it "is false for everything else" do
- Gem.stub(:path).and_return(["/abc/xyz"])
+ allow(Gem).to receive(:path).and_return(["/abc/xyz"])
frame = StackFrame.new("/abc/nope", 123, "foo")
- frame.should_not be_gem
+ expect(frame).not_to be_gem
end
end
context "#application_path" do
it "chops off the application root" do
- BetterErrors.stub(:application_root).and_return("/abc/xyz")
+ allow(BetterErrors).to receive(:application_root).and_return("/abc/xyz")
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
- frame.application_path.should == "app/controllers/crap_controller.rb"
+ expect(frame.application_path).to eq("app/controllers/crap_controller.rb")
end
end
context "#gem_path" do
it "chops of the gem path and stick (gem) there" do
- Gem.stub(:path).and_return(["/abc/xyz"])
+ allow(Gem).to receive(:path).and_return(["/abc/xyz"])
frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
- frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb"
+ expect(frame.gem_path).to eq("whatever (1.2.3) lib/whatever.rb")
end
it "prioritizes gem path over application path" do
- BetterErrors.stub(:application_root).and_return("/abc/xyz")
- Gem.stub(:path).and_return(["/abc/xyz/vendor"])
+ allow(BetterErrors).to receive(:application_root).and_return("/abc/xyz")
+ allow(Gem).to receive(:path).and_return(["/abc/xyz/vendor"])
frame = StackFrame.new("/abc/xyz/vendor/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
- frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb"
+ expect(frame.gem_path).to eq("whatever (1.2.3) lib/whatever.rb")
end
end
context "#pretty_path" do
it "returns #application_path for application paths" do
- BetterErrors.stub(:application_root).and_return("/abc/xyz")
+ allow(BetterErrors).to receive(:application_root).and_return("/abc/xyz")
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
- frame.pretty_path.should == frame.application_path
+ expect(frame.pretty_path).to eq(frame.application_path)
end
it "returns #gem_path for gem paths" do
- Gem.stub(:path).and_return(["/abc/xyz"])
+ allow(Gem).to receive(:path).and_return(["/abc/xyz"])
frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
- frame.pretty_path.should == frame.gem_path
+ expect(frame.pretty_path).to eq(frame.gem_path)
+ end
+ end
+
+ context "#local_variable" do
+ it "returns exception details when #get_local_variable raises NameError" do
+ frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
+ allow(frame).to receive(:get_local_variable).and_raise(NameError.new("details"))
+ expect(frame.local_variable("foo")).to eq("NameError: details")
+ end
+
+ it "returns exception details when #eval_local_variable raises NameError" do
+ frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
+ allow(frame).to receive(:eval_local_variable).and_raise(NameError.new("details"))
+ expect(frame.local_variable("foo")).to eq("NameError: details")
+ end
+
+ it "raises on non-NameErrors" do
+ frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
+ allow(frame).to receive(:get_local_variable).and_raise(ArgumentError)
+ expect { frame.local_variable("foo") }.to raise_error(ArgumentError)
end
end
@@ -87,36 +107,36 @@ module BetterErrors
rescue SyntaxError => syntax_error
end
frames = StackFrame.from_exception(syntax_error)
- frames.first.filename.should == "my_file.rb"
- frames.first.line.should == 123
+ expect(frames.first.filename).to eq("my_file.rb")
+ expect(frames.first.line).to eq(123)
end
it "doesn't blow up if no method name is given" do
error = StandardError.allocate
- error.stub(:backtrace).and_return(["foo.rb:123"])
+ allow(error).to receive(:backtrace).and_return(["foo.rb:123"])
frames = StackFrame.from_exception(error)
- frames.first.filename.should == "foo.rb"
- frames.first.line.should == 123
+ expect(frames.first.filename).to eq("foo.rb")
+ expect(frames.first.line).to eq(123)
- error.stub(:backtrace).and_return(["foo.rb:123: this is an error message"])
+ allow(error).to receive(:backtrace).and_return(["foo.rb:123: this is an error message"])
frames = StackFrame.from_exception(error)
- frames.first.filename.should == "foo.rb"
- frames.first.line.should == 123
+ expect(frames.first.filename).to eq("foo.rb")
+ expect(frames.first.line).to eq(123)
end
it "ignores a backtrace line if its format doesn't make any sense at all" do
error = StandardError.allocate
- error.stub(:backtrace).and_return(["foo.rb:123:in `foo'", "C:in `find'", "bar.rb:123:in `bar'"])
+ allow(error).to receive(:backtrace).and_return(["foo.rb:123:in `foo'", "C:in `find'", "bar.rb:123:in `bar'"])
frames = StackFrame.from_exception(error)
- frames.count.should == 2
+ expect(frames.count).to eq(2)
end
it "doesn't blow up if a filename contains a colon" do
error = StandardError.allocate
- error.stub(:backtrace).and_return(["crap:filename.rb:123"])
+ allow(error).to receive(:backtrace).and_return(["crap:filename.rb:123"])
frames = StackFrame.from_exception(error)
- frames.first.filename.should == "crap:filename.rb"
+ expect(frames.first.filename).to eq("crap:filename.rb")
end
it "doesn't blow up with a BasicObject as frame binding" do
@@ -125,7 +145,7 @@ def obj.my_binding
::Kernel.binding
end
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index", obj.my_binding)
- frame.class_name.should == 'BasicObject'
+ expect(frame.class_name).to eq('BasicObject')
end
it "sets method names properly" do
@@ -140,11 +160,11 @@ def obj.my_method
frame = StackFrame.from_exception(obj.my_method).first
if BetterErrors.binding_of_caller_available?
- frame.method_name.should == "#my_method"
- frame.class_name.should == "String"
+ expect(frame.method_name).to eq("#my_method")
+ expect(frame.class_name).to eq("String")
else
- frame.method_name.should == "my_method"
- frame.class_name.should == nil
+ expect(frame.method_name).to eq("my_method")
+ expect(frame.class_name).to eq(nil)
end
end
diff --git a/spec/better_errors_spec.rb b/spec/better_errors_spec.rb
index e9b26b67..9d796105 100644
--- a/spec/better_errors_spec.rb
+++ b/spec/better_errors_spec.rb
@@ -3,38 +3,45 @@
describe BetterErrors do
context ".editor" do
it "defaults to textmate" do
- subject.editor["foo.rb", 123].should == "txmt://open?url=file://foo.rb&line=123"
+ expect(subject.editor["foo.rb", 123]).to eq("txmt://open?url=file://foo.rb&line=123")
end
it "url escapes the filename" do
- subject.editor["&.rb", 0].should == "txmt://open?url=file://%26.rb&line=0"
+ expect(subject.editor["&.rb", 0]).to eq("txmt://open?url=file://%26.rb&line=0")
end
[:emacs, :emacsclient].each do |editor|
it "uses emacs:// scheme when set to #{editor.inspect}" do
subject.editor = editor
- subject.editor[].should start_with "emacs://"
+ expect(subject.editor[]).to start_with "emacs://"
end
end
[:macvim, :mvim].each do |editor|
it "uses mvim:// scheme when set to #{editor.inspect}" do
subject.editor = editor
- subject.editor[].should start_with "mvim://"
+ expect(subject.editor[]).to start_with "mvim://"
end
end
[:sublime, :subl, :st].each do |editor|
it "uses subl:// scheme when set to #{editor.inspect}" do
subject.editor = editor
- subject.editor[].should start_with "subl://"
+ expect(subject.editor[]).to start_with "subl://"
end
end
[:textmate, :txmt, :tm].each do |editor|
it "uses txmt:// scheme when set to #{editor.inspect}" do
subject.editor = editor
- subject.editor[].should start_with "txmt://"
+ expect(subject.editor[]).to start_with "txmt://"
+ end
+ end
+
+ [:atom].each do |editor|
+ it "uses atom:// scheme when set to #{editor.inspect}" do
+ subject.editor = editor
+ expect(subject.editor[]).to start_with "atom://"
end
end
@@ -42,7 +49,7 @@
it "uses emacs:// scheme when EDITOR=#{editor}" do
ENV["EDITOR"] = editor
subject.editor = subject.default_editor
- subject.editor[].should start_with "emacs://"
+ expect(subject.editor[]).to start_with "emacs://"
end
end
@@ -50,15 +57,15 @@
it "uses mvim:// scheme when EDITOR=#{editor}" do
ENV["EDITOR"] = editor
subject.editor = subject.default_editor
- subject.editor[].should start_with "mvim://"
+ expect(subject.editor[]).to start_with "mvim://"
end
end
["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |editor|
- it "uses mvim:// scheme when EDITOR=#{editor}" do
+ it "uses subl:// scheme when EDITOR=#{editor}" do
ENV["EDITOR"] = editor
subject.editor = subject.default_editor
- subject.editor[].should start_with "subl://"
+ expect(subject.editor[]).to start_with "subl://"
end
end
@@ -66,7 +73,40 @@
it "uses txmt:// scheme when EDITOR=#{editor}" do
ENV["EDITOR"] = editor
subject.editor = subject.default_editor
- subject.editor[].should start_with "txmt://"
+ expect(subject.editor[]).to start_with "txmt://"
+ end
+ end
+
+
+ ["atom -w", "/usr/bin/atom -w"].each do |editor|
+ it "uses atom:// scheme when EDITOR=#{editor}" do
+ ENV["EDITOR"] = editor
+ subject.editor = subject.default_editor
+ expect(subject.editor[]).to start_with "atom://"
+ end
+ end
+
+ ["mine"].each do |editor|
+ it "uses x-mine:// scheme when EDITOR=#{editor}" do
+ ENV["EDITOR"] = editor
+ subject.editor = subject.default_editor
+ expect(subject.editor[]).to start_with "x-mine://"
+ end
+ end
+
+ ["idea"].each do |editor|
+ it "uses idea:// scheme when EDITOR=#{editor}" do
+ ENV["EDITOR"] = editor
+ subject.editor = subject.default_editor
+ expect(subject.editor[]).to start_with "idea://"
+ end
+ end
+
+ ["vscode", "code"].each do |editor|
+ it "uses vscode:// scheme when EDITOR=#{editor}" do
+ ENV["EDITOR"] = editor
+ subject.editor = subject.default_editor
+ expect(subject.editor[]).to start_with "vscode://"
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 40d63261..29e5ac18 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,4 +2,26 @@
ENV["EDITOR"] = nil
-require "better_errors"
+# Ruby 2.4.0 and 2.4.1 has a bug with its Coverage module that causes segfaults.
+# https://bugs.ruby-lang.org/issues/13305
+# 2.4.2 should include this patch.
+if ENV['CI']
+ unless RUBY_VERSION == '2.4.0' || RUBY_VERSION == '2.4.1'
+ require 'coveralls'
+ Coveralls.wear! do
+ add_filter 'spec/'
+ end
+ end
+else
+ require 'simplecov'
+ SimpleCov.start
+end
+
+require 'bundler/setup'
+Bundler.require(:default)
+
+require 'rspec-html-matchers'
+
+RSpec.configure do |config|
+ config.include RSpecHtmlMatchers
+end