better_errors 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,33 +1,31 @@
1
1
  # Better Errors
2
2
 
3
- Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails.
3
+ Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails in any Rack app as Rack middleware.
4
4
 
5
- ![image](http://i.imgur.com/quHUZ.png)
5
+ ![image](http://i.imgur.com/urVDW.png)
6
6
 
7
7
  ## Features
8
8
 
9
9
  * Full stack trace
10
10
  * Source code inspection for all stack frames (with highlighting)
11
11
  * Local and instance variable inspection
12
- * Ruby console on every stack frame
12
+ * Live REPL on every stack frame
13
13
 
14
14
  ## Installation
15
15
 
16
16
  Add this line to your application's Gemfile (under the **development** group):
17
17
 
18
- gem 'better_errors'
19
-
20
- And then execute:
21
-
22
- $ bundle
23
-
24
- Or install it yourself as:
25
-
26
- $ gem install better_errors
18
+ ```ruby
19
+ group :development do
20
+ gem "better_errors"
21
+ end
22
+ ```
27
23
 
28
- If you would like to use Better Errors' **advanced features**, you need to install the `binding_of_caller` gem:
24
+ If you would like to use Better Errors' **advanced features** (REPL, local/instance variable inspection, pretty stack frame names), you need to add the [`binding_of_caller`](https://github.com/banister/binding_of_caller) gem to your Gemfile:
29
25
 
30
- $ gem install binding_of_caller
26
+ ```ruby
27
+ gem "binding_of_caller"
28
+ ```
31
29
 
32
30
  This is an optional dependency however, and Better Errors will work without it.
33
31
 
@@ -51,6 +49,19 @@ get "/" do
51
49
  end
52
50
  ```
53
51
 
52
+ ## Compatibility
53
+
54
+ * **Supported**
55
+ * MRI 1.9.2, 1.9.3
56
+ * JRuby (1.9 mode) - *advanced features unsupported*
57
+ * Rubinius (1.9 mode) - *advanced features unsupported*
58
+ * **Coming soon**
59
+ * MRI 2.0.0 - the official API for grabbing caller bindings is slated for MRI 2.0.0, but it has not been implemented yet
60
+
61
+ ## Known issues
62
+
63
+ * Calling `yield` from the REPL segfaults MRI 1.9.x.
64
+
54
65
  ## Contributing
55
66
 
56
67
  1. Fork it
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.version = BetterErrors::VERSION
9
9
  s.authors = ["Charlie Somerville"]
10
10
  s.email = ["charlie@charliesomerville.com"]
11
- s.description = %q{Better Errors gives Rails a better error page.}
12
- s.summary = %q{Better Errors gives Rails a better error page}
11
+ s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.}
12
+ s.summary = %q{Better error page for Rails and other Rack apps}
13
13
  s.homepage = "https://github.com/charliesome/better_errors"
14
14
  s.license = "MIT"
15
15
 
@@ -19,8 +19,10 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_development_dependency "rake"
21
21
 
22
- s.add_dependency "erubis"
23
- s.add_dependency "coderay"
24
- # optional dependency:
22
+ s.add_dependency "erubis", ">= 2.7.0"
23
+ s.add_dependency "coderay", ">= 1.0.0"
24
+
25
+ # optional dependencies:
25
26
  # s.add_dependency "binding_of_caller"
27
+ # s.add_dependency "pry"
26
28
  end
@@ -0,0 +1,60 @@
1
+ module BetterErrors
2
+ class CodeFormatter
3
+ FILE_TYPES = {
4
+ ".rb" => :ruby,
5
+ "" => :ruby,
6
+ ".html" => :html,
7
+ ".erb" => :erb,
8
+ ".haml" => :haml
9
+ }
10
+
11
+ attr_reader :filename, :line, :context
12
+
13
+ def initialize(filename, line, context = 5)
14
+ @filename = filename
15
+ @line = line
16
+ @context = context
17
+ end
18
+
19
+ def html
20
+ %{<div class="code">#{formatted_lines.join}</div>}
21
+ rescue Errno::ENOENT, Errno::EINVAL
22
+ source_unavailable
23
+ end
24
+
25
+ def source_unavailable
26
+ "<p>Source unavailable</p>"
27
+ end
28
+
29
+ def coderay_scanner
30
+ ext = File.extname(filename)
31
+ FILE_TYPES[ext] || :text
32
+ end
33
+
34
+ def formatted_lines
35
+ line_range.zip(highlighted_lines).map do |current_line, str|
36
+ class_name = current_line == line ? "highlight" : ""
37
+ sprintf '<pre class="%s">%5d %s</pre>', class_name, current_line, str
38
+ end
39
+ end
40
+
41
+ def highlighted_lines
42
+ CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
43
+ end
44
+
45
+ def context_lines
46
+ range = line_range
47
+ source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
48
+ end
49
+
50
+ def source_lines
51
+ @source_lines ||= File.readlines(filename)
52
+ end
53
+
54
+ def line_range
55
+ min = [line - context, 1].max
56
+ max = [line + context, source_lines.count].min
57
+ min..max
58
+ end
59
+ end
60
+ end
@@ -1,17 +1,21 @@
1
1
  class Exception
2
- attr_reader :__better_errors_bindings_stack
3
-
4
2
  original_initialize = instance_method(:initialize)
5
3
 
6
- define_method :initialize do |*args|
7
- unless Thread.current[:__better_errors_exception_lock]
8
- Thread.current[:__better_errors_exception_lock] = true
9
- begin
10
- @__better_errors_bindings_stack = binding.callers.drop(1)
11
- ensure
12
- Thread.current[:__better_errors_exception_lock] = false
4
+ if BetterErrors.binding_of_caller_available?
5
+ define_method :initialize do |*args|
6
+ unless Thread.current[:__better_errors_exception_lock]
7
+ Thread.current[:__better_errors_exception_lock] = true
8
+ begin
9
+ @__better_errors_bindings_stack = binding.callers.drop(1)
10
+ ensure
11
+ Thread.current[:__better_errors_exception_lock] = false
12
+ end
13
13
  end
14
+ original_initialize.bind(self).call(*args)
14
15
  end
15
- original_initialize.bind(self).call(*args)
16
16
  end
17
- end
17
+
18
+ def __better_errors_bindings_stack
19
+ @__better_errors_bindings_stack || []
20
+ end
21
+ end
@@ -10,12 +10,13 @@ module BetterErrors
10
10
  Erubis::EscapedEruby.new(File.read(template_path(template_name)))
11
11
  end
12
12
 
13
- attr_reader :exception, :env
13
+ attr_reader :exception, :env, :repls
14
14
 
15
15
  def initialize(exception, env)
16
16
  @exception = real_exception(exception)
17
17
  @env = env
18
18
  @start_time = Time.now.to_f
19
+ @repls = []
19
20
  end
20
21
 
21
22
  def render(template_name = "main")
@@ -30,13 +31,22 @@ module BetterErrors
30
31
 
31
32
  def do_eval(opts)
32
33
  index = opts["index"].to_i
33
- response = begin
34
- result = backtrace_frames[index].frame_binding.eval(opts["source"])
35
- { result: result.inspect }
36
- rescue Exception => e
37
- { error: (e.inspect rescue e.class.name rescue "Exception") }
38
- end
39
- response.merge(highlighted_input: CodeRay.scan(opts["source"], :ruby).div(wrap: nil))
34
+ code = opts["source"]
35
+
36
+ unless binding = backtrace_frames[index].frame_binding
37
+ return { error: "REPL unavailable in this stack frame" }
38
+ end
39
+
40
+ result, prompt =
41
+ (@repls[index] ||= REPL.provider.new(binding)).send_input(code)
42
+
43
+ { result: result,
44
+ prompt: prompt,
45
+ highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
46
+ end
47
+
48
+ def backtrace_frames
49
+ @backtrace_frames ||= StackFrame.from_exception(exception)
40
50
  end
41
51
 
42
52
  private
@@ -51,48 +61,9 @@ module BetterErrors
51
61
  def request_path
52
62
  env["REQUEST_PATH"]
53
63
  end
54
-
55
- def backtrace_frames
56
- @backtrace_frames ||= ErrorFrame.from_exception(exception)
57
- end
58
-
59
- def coderay_scanner_for_ext(ext)
60
- case ext
61
- when "rb"; :ruby
62
- when "html"; :html
63
- when "erb"; :erb
64
- when "haml"; :haml
65
- end
66
- end
67
-
68
- def file_extension(filename)
69
- filename.split(".").last
70
- end
71
-
72
- def code_extract(frame, lines_of_context = 5)
73
- lines = File.readlines(frame.filename)
74
- min_line = [1, frame.line - lines_of_context].max - 1
75
- max_line = [frame.line + lines_of_context, lines.count + 1].min - 1
76
- [min_line, max_line, lines[min_line..max_line].join]
77
- end
78
64
 
79
65
  def highlighted_code_block(frame)
80
- ext = file_extension(frame.filename)
81
- scanner = coderay_scanner_for_ext(ext)
82
- min_line, max_line, code = code_extract(frame)
83
- highlighted_code = CodeRay.scan(code, scanner).div wrap: nil
84
- "".tap do |html|
85
- html << "<div class='code'>"
86
- highlighted_code.each_line.each_with_index do |str, index|
87
- if min_line + index + 1 == frame.line
88
- html << "<pre class='highlight'>"
89
- else
90
- html << "<pre>"
91
- end
92
- html << sprintf("%5d", min_line + index + 1) << " " << str << "</pre>"
93
- end
94
- html << "</div>"
95
- end
66
+ CodeFormatter.new(frame.filename, frame.line).html
96
67
  end
97
68
  end
98
69
  end
@@ -8,8 +8,11 @@ module BetterErrors
8
8
  end
9
9
 
10
10
  def call(env)
11
- if env["REQUEST_PATH"] =~ %r{\A/__better_errors/(?<oid>\d+)/(?<method>\w+)\z}
11
+ case env["PATH_INFO"]
12
+ when %r{\A/__better_errors/(?<oid>\d+)/(?<method>\w+)\z}
12
13
  internal_call env, $~
14
+ when %r{\A/__better_errors/?\z}
15
+ show_error_page env
13
16
  else
14
17
  app_call env
15
18
  end
@@ -20,8 +23,24 @@ module BetterErrors
20
23
  @app.call env
21
24
  rescue Exception => ex
22
25
  @error_page = @handler.new ex, env
26
+ log_exception
27
+ show_error_page(env)
28
+ end
29
+
30
+ def show_error_page(env)
23
31
  [500, { "Content-Type" => "text/html; charset=utf-8" }, [@error_page.render]]
24
32
  end
33
+
34
+ def log_exception
35
+ return unless BetterErrors.logger
36
+
37
+ message = "\n#{@error_page.exception.class} - #{@error_page.exception.message}:\n"
38
+ @error_page.backtrace_frames.each do |frame|
39
+ message << " #{frame}\n"
40
+ end
41
+
42
+ BetterErrors.logger.fatal message
43
+ end
25
44
 
26
45
  def internal_call(env, opts)
27
46
  if opts[:oid].to_i != @error_page.object_id
@@ -1,10 +1,11 @@
1
1
  module BetterErrors
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "better_errors.configure_rails_initialization" do
4
- middleware = Rails.application.middleware
5
- middleware.use BetterErrors::Middleware
6
-
7
- BetterErrors.application_root = Rails.root.to_s
4
+ unless Rails.env.production?
5
+ Rails.application.middleware.use BetterErrors::Middleware
6
+ BetterErrors.logger = Rails.logger
7
+ BetterErrors.application_root = Rails.root.to_s
8
+ end
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,20 @@
1
+ module BetterErrors
2
+ module REPL
3
+ class Basic
4
+ def initialize(binding)
5
+ @binding = binding
6
+ end
7
+
8
+ def send_input(str)
9
+ [execute(str), ">>"]
10
+ end
11
+
12
+ private
13
+ def execute(str)
14
+ "=> #{@binding.eval(str).inspect}\n"
15
+ rescue Exception => e
16
+ "!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,67 @@
1
+ require "fiber"
2
+ require "pry"
3
+
4
+ module BetterErrors
5
+ module REPL
6
+ class Pry
7
+ class Input
8
+ def readline
9
+ Fiber.yield
10
+ end
11
+ end
12
+
13
+ class Output
14
+ def initialize
15
+ @buffer = ""
16
+ end
17
+
18
+ def puts(*args)
19
+ args.each do |arg|
20
+ @buffer << "#{arg.chomp}\n"
21
+ end
22
+ end
23
+
24
+ def tty?
25
+ false
26
+ end
27
+
28
+ def read_buffer
29
+ @buffer
30
+ ensure
31
+ @buffer = ""
32
+ end
33
+ end
34
+
35
+ def initialize(binding)
36
+ @fiber = Fiber.new do
37
+ @pry.repl binding
38
+ end
39
+ @input = Input.new
40
+ @output = Output.new
41
+ @pry = ::Pry.new input: @input, output: @output
42
+ @pry.hooks.clear_all
43
+ @continued_expression = false
44
+ @pry.hooks.add_hook :after_read, "better_errors hacky hook" do
45
+ @continued_expression = false
46
+ end
47
+ @fiber.resume
48
+ end
49
+
50
+ def pry_indent
51
+ @pry.instance_variable_get(:@indent)
52
+ end
53
+
54
+ def send_input(str)
55
+ old_pry_config_color = ::Pry.config.color
56
+ ::Pry.config.color = false
57
+ @continued_expression = true
58
+ @fiber.resume "#{str}\n"
59
+ # TODO - indent with `pry_indent.current_prefix`
60
+ # TODO - use proper pry prompt
61
+ [@output.read_buffer, @continued_expression ? ".." : ">>"]
62
+ ensure
63
+ ::Pry.config.color = old_pry_config_color
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ module BetterErrors
2
+ module REPL
3
+ PROVIDERS = [
4
+ { impl: "better_errors/repl/pry",
5
+ const: :Pry },
6
+ { impl: "better_errors/repl/basic",
7
+ const: :Basic },
8
+ ]
9
+
10
+ def self.provider
11
+ @provider ||= const_get detect[:const]
12
+ end
13
+
14
+ def self.detect
15
+ PROVIDERS.find do |prov|
16
+ test_provider prov
17
+ end
18
+ end
19
+
20
+ def self.test_provider(provider)
21
+ require provider[:impl]
22
+ true
23
+ rescue LoadError
24
+ false
25
+ end
26
+ end
27
+ end
@@ -1,12 +1,18 @@
1
1
  module BetterErrors
2
- class ErrorFrame
2
+ class StackFrame
3
3
  def self.from_exception(exception)
4
+ idx_offset = 0
4
5
  exception.backtrace.each_with_index.map { |frame, idx|
5
- next unless frame =~ /\A(.*):(\d*):in `(.*)'\z/
6
- if BetterErrors.binding_of_caller_available?
7
- b = exception.__better_errors_bindings_stack[idx]
6
+ frame_binding = exception.__better_errors_bindings_stack[idx - idx_offset]
7
+ md = /\A(?<file>.*):(?<line>\d*):in `(?<name>.*)'\z/.match(frame)
8
+
9
+ # prevent mismatching frames in the backtrace with the binding stack
10
+ if frame_binding and frame_binding.eval("__FILE__") != md[:file]
11
+ idx_offset += 1
12
+ frame_binding = nil
8
13
  end
9
- ErrorFrame.new($1, $2.to_i, $3, b)
14
+
15
+ StackFrame.new(md[:file], md[:line].to_i, md[:name], frame_binding)
10
16
  }.compact
11
17
  end
12
18
 
@@ -22,7 +28,8 @@ module BetterErrors
22
28
  end
23
29
 
24
30
  def application?
25
- starts_with? filename, BetterErrors.application_root if BetterErrors.application_root
31
+ root = BetterErrors.application_root
32
+ starts_with? filename, root if root
26
33
  end
27
34
 
28
35
  def application_path
@@ -73,14 +80,20 @@ module BetterErrors
73
80
 
74
81
  def instance_variables
75
82
  return {} unless frame_binding
76
- Hash[frame_binding.eval("instance_variables").map { |x| [x, frame_binding.eval(x.to_s)] }]
83
+ Hash[frame_binding.eval("instance_variables").map { |x|
84
+ [x, frame_binding.eval(x.to_s)]
85
+ }]
86
+ end
87
+
88
+ def to_s
89
+ "#{pretty_path}:#{line}:in `#{name}'"
77
90
  end
78
91
 
79
92
  private
80
93
  def set_pretty_method_name
81
94
  name =~ /\A(block (\([^)]+\) )?in )?/
82
95
  recv = frame_binding.eval("self")
83
- method = frame_binding.eval("__method__")
96
+ return unless method = frame_binding.eval("__method__")
84
97
  @name = if recv.is_a? Module
85
98
  "#{$1}#{recv}.#{method}"
86
99
  else
@@ -105,6 +105,7 @@
105
105
  font-family:Monaco, Incosolata, Consolas, monospace;
106
106
  font-size:14px;
107
107
  margin-bottom:4px;
108
+ word-wrap:break-word;
108
109
  }
109
110
  .location {
110
111
  color:#888888;
@@ -243,15 +244,22 @@
243
244
  <div class="location"><span class="filename"><%= frame.pretty_path %></span>, line <span class="line"><%= frame.line %></span></div>
244
245
  <%== highlighted_code_block frame %>
245
246
 
246
- <div class="repl">
247
- <h3>REPL</h3>
248
- <div class="console">
249
- <pre></pre>
250
- <div class="prompt">&gt;&gt; <input/></div>
247
+ <% if BetterErrors.binding_of_caller_available? %>
248
+ <div class="repl">
249
+ <h3>REPL</h3>
250
+ <div class="console">
251
+ <pre></pre>
252
+ <div class="prompt"><span>&gt;&gt;</span> <input/></div>
253
+ </div>
251
254
  </div>
252
- </div>
253
-
254
- <div class="variable_info"></div>
255
+
256
+ <div class="variable_info"></div>
257
+ <% else %>
258
+ <h3>Advanced features unavailable</h3>
259
+ <p class="error">
260
+ You must add <code>gem "binding_of_caller"</code> to your Gemfile to enable the REPL and local/instance variable inspection.
261
+ </p>
262
+ <% end %>
255
263
  </div>
256
264
  <% end %>
257
265
  <div style="clear:both"></div>
@@ -259,19 +267,16 @@
259
267
  </body>
260
268
  <script>
261
269
  (function() {
262
- var oid = <%== object_id.to_s.inspect %>;
270
+ var OID = <%== object_id.to_s.inspect %>;
263
271
 
264
- var previous = null;
272
+ var previousFrame = null;
265
273
  var previousFrameInfo = null;
266
- var frames = document.querySelectorAll("ul.frames li");
267
- var frameInfos = document.querySelectorAll(".frame_info");
268
-
269
- var header = document.querySelector("header");
270
- var headerHeight = header.offsetHeight;
271
-
272
- apiCall = function(method, opts, cb) {
274
+ var allFrames = document.querySelectorAll("ul.frames li");
275
+ var allFrameInfos = document.querySelectorAll(".frame_info");
276
+
277
+ function apiCall(method, opts, cb) {
273
278
  var req = new XMLHttpRequest();
274
- req.open("POST", "/__better_errors/" + oid + "/" + method, true);
279
+ req.open("POST", "/__better_errors/" + OID + "/" + method, true);
275
280
  req.setRequestHeader("Content-Type", "application/json");
276
281
  req.send(JSON.stringify(opts));
277
282
  req.onreadystatechange = function() {
@@ -286,11 +291,118 @@
286
291
  return html.replace(/&/, "&amp;").replace(/</g, "&lt;");
287
292
  }
288
293
 
289
- function selectFrameInfo(index) {
290
- var el = document.getElementById("frame_info_" + index);
294
+ function REPL(index) {
295
+ this.index = index;
296
+
297
+ this.previousCommands = [];
298
+ this.previousCommandOffset = 0;
299
+ }
300
+
301
+ REPL.all = [];
302
+
303
+ REPL.prototype.install = function(containerElement) {
304
+ this.container = containerElement;
305
+
306
+ this.promptElement = this.container.querySelector(".prompt span");
307
+ this.inputElement = this.container.querySelector("input");
308
+ this.outputElement = this.container.querySelector("pre");
309
+
310
+ this.inputElement.onkeydown = this.onKeyDown.bind(this);
311
+
312
+ this.setPrompt(">>");
313
+
314
+ REPL.all[this.index] = this;
315
+ }
316
+
317
+ REPL.prototype.focus = function() {
318
+ this.inputElement.focus();
319
+ };
320
+
321
+ REPL.prototype.setPrompt = function(prompt) {
322
+ this._prompt = prompt;
323
+ this.promptElement.innerHTML = escapeHTML(prompt);
324
+ };
325
+
326
+ REPL.prototype.getInput = function() {
327
+ return this.inputElement.value;
328
+ };
329
+
330
+ REPL.prototype.setInput = function(text) {
331
+ this.inputElement.value = text;
332
+
333
+ if(this.inputElement.setSelectionRange) {
334
+ // set cursor to end of input
335
+ this.inputElement.setSelectionRange(text.length, text.length);
336
+ }
337
+ };
338
+
339
+ REPL.prototype.writeRawOutput = function(output) {
340
+ this.outputElement.innerHTML += output;
341
+ this.outputElement.scrollTop = this.outputElement.scrollHeight;
342
+ };
343
+
344
+ REPL.prototype.writeOutput = function(output) {
345
+ this.writeRawOutput(escapeHTML(output));
346
+ };
347
+
348
+ REPL.prototype.sendInput = function(line) {
349
+ var self = this;
350
+ apiCall("eval", { "index": this.index, source: line }, function(response) {
351
+ if(response.error) {
352
+ self.writeOutput(response.error + "\n");
353
+ }
354
+ self.writeOutput(self._prompt + " ");
355
+ self.writeRawOutput(response.highlighted_input + "\n");
356
+ self.writeOutput(response.result);
357
+ self.setPrompt(response.prompt);
358
+ });
359
+ };
360
+
361
+ REPL.prototype.onEnterKey = function() {
362
+ var text = this.getInput();
363
+ if(text != "" && text !== undefined) {
364
+ this.previousCommandOffset = this.previousCommands.push(text);
365
+ }
366
+ this.setInput("");
367
+ this.sendInput(text);
368
+ };
369
+
370
+ REPL.prototype.onNavigateHistory = function(direction) {
371
+ this.previousCommandOffset += direction;
372
+
373
+ if(this.previousCommandOffset < 0) {
374
+ this.previousCommandOffset = -1;
375
+ this.setInput("");
376
+ return;
377
+ }
378
+
379
+ if(this.previousCommandOffset >= this.previousCommands.length) {
380
+ this.previousCommandOffset = this.previousCommands.length;
381
+ this.setInput("");
382
+ return;
383
+ }
291
384
 
385
+ this.setInput(this.previousCommands[this.previousCommandOffset]);
386
+ };
387
+
388
+ REPL.prototype.onKeyDown = function(ev) {
389
+ if(ev.keyCode == 13) {
390
+ this.onEnterKey();
391
+ } else if(ev.keyCode == 38) {
392
+ // the user pressed the up arrow.
393
+ this.onNavigateHistory(-1);
394
+ return false;
395
+ } else if(ev.keyCode == 40) {
396
+ // the user pressed the down arrow.
397
+ this.onNavigateHistory(1);
398
+ return false;
399
+ }
400
+ };
401
+
402
+ function populateVariableInfo(index) {
403
+ var el = allFrameInfos[index];
292
404
  var varInfo = el.querySelector(".variable_info");
293
- if(varInfo.innerHTML == "") {
405
+ if(varInfo && varInfo.innerHTML == "") {
294
406
  apiCall("variables", { "index": index }, function(response) {
295
407
  if(response.error) {
296
408
  varInfo.innerHTML = "<span class='error'>" + escapeHTML(response.error) + "</span>";
@@ -299,75 +411,69 @@
299
411
  }
300
412
  });
301
413
  }
414
+ }
415
+
416
+ function selectFrameInfo(index) {
417
+ populateVariableInfo(index);
302
418
 
303
419
  if(previousFrameInfo) {
304
420
  previousFrameInfo.style.display = "none";
305
421
  }
306
- previousFrameInfo = el;
422
+ previousFrameInfo = allFrameInfos[index];
307
423
  previousFrameInfo.style.display = "block";
308
424
 
309
- el.querySelector(".repl input").focus();
425
+ if(REPL.all[index]) {
426
+ REPL.all[index].focus();
427
+ }
310
428
  }
311
429
 
312
- for(var i = 0; i < frames.length; i++) {
313
- (function(index, el, frameInfo) {
430
+ for(var i = 0; i < allFrames.length; i++) {
431
+ (function(i, el) {
432
+ var el = allFrames[i];
314
433
  el.onclick = function() {
315
- if(previous) {
316
- previous.className = "";
434
+ if(previousFrame) {
435
+ previousFrame.className = "";
317
436
  }
318
437
  el.className = "selected";
319
- previous = el;
438
+ previousFrame = el;
320
439
 
321
440
  selectFrameInfo(el.attributes["data-index"].value);
322
441
  };
323
- var replPre = frameInfo.querySelector(".repl pre");
324
- var replInput = frameInfo.querySelector(".repl input");
325
- replInput.onkeydown = function(ev) {
326
- if(ev.keyCode == 13) {
327
- var text = replInput.value;
328
- replInput.value = "";
329
- apiCall("eval", { "index": index, source: text }, function(response) {
330
- replPre.innerHTML += ">> " + response.highlighted_input + "\n";
331
- if(response.error) {
332
- replPre.innerHTML += "!! " + escapeHTML(response.error) + "\n";
333
- } else {
334
- replPre.innerHTML += "=> " + escapeHTML(response.result) + "\n";
335
- }
336
- replPre.scrollTop = replPre.offsetHeight;
337
- });
338
- }
339
- };
340
- })(i, frames[i], frameInfos[i]);
442
+ var repl = allFrameInfos[i].querySelector(".repl .console");
443
+ if(repl) {
444
+ new REPL(i).install(repl);
445
+ }
446
+ })(i);
341
447
  }
342
448
 
343
- document.querySelector(".frames li:first-child").click();
449
+ allFrames[0].click();
344
450
 
345
- var applicationFrames = document.getElementById("application_frames");
346
- var allFrames = document.getElementById("all_frames");
451
+ var applicationFramesButton = document.getElementById("application_frames");
452
+ var allFramesButton = document.getElementById("all_frames");
347
453
 
348
- applicationFrames.onclick = function() {
349
- allFrames.className = "";
350
- applicationFrames.className = "selected";
351
- for(var i = 0; i < frames.length; i++) {
352
- if(frames[i].attributes["data-context"].value == "application") {
353
- frames[i].style.display = "block";
454
+ applicationFramesButton.onclick = function() {
455
+ allFramesButton.className = "";
456
+ applicationFramesButton.className = "selected";
457
+ for(var i = 0; i < allFrames.length; i++) {
458
+ if(allFrames[i].attributes["data-context"].value == "application") {
459
+ allFrames[i].style.display = "block";
354
460
  } else {
355
- frames[i].style.display = "none";
461
+ allFrames[i].style.display = "none";
356
462
  }
357
463
  }
358
464
  return false;
359
465
  };
360
466
 
361
- allFrames.onclick = function() {
362
- applicationFrames.className = "";
363
- allFrames.className = "selected";
364
- for(var i = 0; i < frames.length; i++) {
365
- frames[i].style.display = "block";
467
+ allFramesButton.onclick = function() {
468
+ applicationFramesButton.className = "";
469
+ allFramesButton.className = "selected";
470
+ for(var i = 0; i < allFrames.length; i++) {
471
+ allFrames[i].style.display = "block";
366
472
  }
367
473
  return false;
368
474
  };
369
475
 
370
- applicationFrames.click();
476
+ applicationFramesButton.click();
371
477
  })();
372
478
  </script>
373
479
  </html>
@@ -1,3 +1,3 @@
1
1
  module BetterErrors
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/better_errors.rb CHANGED
@@ -4,11 +4,13 @@ require "coderay"
4
4
 
5
5
  require "better_errors/version"
6
6
  require "better_errors/error_page"
7
- require "better_errors/error_frame"
7
+ require "better_errors/stack_frame"
8
8
  require "better_errors/middleware"
9
+ require "better_errors/code_formatter"
10
+ require "better_errors/repl"
9
11
 
10
12
  class << BetterErrors
11
- attr_accessor :application_root, :binding_of_caller_available
13
+ attr_accessor :application_root, :binding_of_caller_available, :logger
12
14
 
13
15
  alias_method :binding_of_caller_available?, :binding_of_caller_available
14
16
  end
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ module BetterErrors
4
+ describe CodeFormatter do
5
+ let(:filename) { File.expand_path("../support/my_source.rb", __FILE__) }
6
+
7
+ let(:formatter) { CodeFormatter.new(filename, 8) }
8
+
9
+ it "should pick an appropriate scanner" do
10
+ formatter.coderay_scanner.should == :ruby
11
+ end
12
+
13
+ it "should show 5 lines of context" do
14
+ formatter.line_range.should == (3..13)
15
+
16
+ formatter.context_lines.should == [
17
+ "three\n",
18
+ "four\n",
19
+ "five\n",
20
+ "six\n",
21
+ "seven\n",
22
+ "eight\n",
23
+ "nine\n",
24
+ "ten\n",
25
+ "eleven\n",
26
+ "twelve\n",
27
+ "thirteen\n"
28
+ ]
29
+ end
30
+
31
+ it "should highlight the erroring line" do
32
+ formatter.html.should =~ /highlight.*eight/
33
+ end
34
+
35
+ it "should work when the line is right on the edge" do
36
+ formatter = CodeFormatter.new(filename, 20)
37
+ formatter.line_range.should == (15..20)
38
+ formatter.html.should_not == formatter.source_unavailable
39
+ end
40
+
41
+ it "should not barf when the lines don't make any sense" do
42
+ formatter = CodeFormatter.new(filename, 999)
43
+ formatter.html.should == formatter.source_unavailable
44
+ end
45
+
46
+ it "should not barf when the file doesn't exist" do
47
+ formatter = CodeFormatter.new("fkdguhskd7e l", 1)
48
+ formatter.html.should == formatter.source_unavailable
49
+ end
50
+ end
51
+ end
@@ -7,6 +7,16 @@ module BetterErrors
7
7
  let(:error_page) { ErrorPage.new exception, { "REQUEST_PATH" => "/some/path" } }
8
8
 
9
9
  let(:response) { error_page.render }
10
+
11
+ let(:empty_binding) {
12
+ local_a = :value_for_local_a
13
+ local_b = :value_for_local_b
14
+
15
+ @inst_c = :value_for_inst_c
16
+ @inst_d = :value_for_inst_d
17
+
18
+ binding
19
+ }
10
20
 
11
21
  it "should include the error message" do
12
22
  response.should include("you divided by zero you silly goose!")
@@ -20,26 +30,31 @@ module BetterErrors
20
30
  response.should include("ZeroDivisionError")
21
31
  end
22
32
 
23
- context "when showing source code" do
24
- before do
25
- exception.stub!(:backtrace).and_return([
26
- "#{File.expand_path("../support/my_source.rb", __FILE__)}:8:in `some_method'"
27
- ])
28
- end
33
+ context "variable inspection" do
34
+ let(:exception) { empty_binding.eval("raise") rescue $! }
29
35
 
30
- it "should show the line where the exception was raised" do
31
- response.should include("8 eight")
36
+ it "should show local variables" do
37
+ html = error_page.do_variables("index" => 0)[:html]
38
+ html.should include("local_a")
39
+ html.should include(":value_for_local_a")
40
+ html.should include("local_b")
41
+ html.should include(":value_for_local_b")
32
42
  end
33
43
 
34
- it "should show five lines of context" do
35
- response.should include("3 three")
36
- response.should include("13 thirteen")
37
- end
38
-
39
- it "should not show more than five lines of context" do
40
- response.should_not include("2 two")
41
- response.should_not include("14 fourteen")
44
+ it "should show instance variables" do
45
+ html = error_page.do_variables("index" => 0)[:html]
46
+ html.should include("inst_c")
47
+ html.should include(":value_for_inst_c")
48
+ html.should include("inst_d")
49
+ html.should include(":value_for_inst_d")
42
50
  end
43
51
  end
52
+
53
+ it "should not die if the source file is not a real filename" do
54
+ exception.stub!(:backtrace).and_return([
55
+ "<internal:prelude>:10:in `spawn_rack_application'"
56
+ ])
57
+ response.should include("Source unavailable")
58
+ end
44
59
  end
45
60
  end
@@ -21,6 +21,14 @@ module BetterErrors
21
21
 
22
22
  headers["Content-Type"].should == "text/html; charset=utf-8"
23
23
  end
24
+
25
+ it "should log the exception" do
26
+ logger = Object.new
27
+ logger.should_receive :fatal
28
+ BetterErrors.stub!(:logger).and_return(logger)
29
+
30
+ app.call({})
31
+ end
24
32
  end
25
33
  end
26
34
  end
@@ -0,0 +1,32 @@
1
+ require "spec_helper"
2
+ require "better_errors/repl/basic"
3
+
4
+ module BetterErrors
5
+ module REPL
6
+ describe Basic do
7
+ let(:fresh_binding) {
8
+ local_a = 123
9
+ binding
10
+ }
11
+
12
+ let(:repl) { Basic.new fresh_binding }
13
+
14
+ it "should evaluate ruby code in a given context" do
15
+ repl.send_input("local_a = 456")
16
+ fresh_binding.eval("local_a").should == 456
17
+ end
18
+
19
+ it "should return a tuple of output and the new prompt" do
20
+ output, prompt = repl.send_input("1 + 2")
21
+ output.should == "=> 3\n"
22
+ prompt.should == ">>"
23
+ end
24
+
25
+ it "should not barf if the code throws an exception" do
26
+ output, prompt = repl.send_input("raise Exception")
27
+ output.should == "!! #<Exception: Exception>\n"
28
+ prompt.should == ">>"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,24 +1,24 @@
1
1
  require "spec_helper"
2
2
 
3
3
  module BetterErrors
4
- describe ErrorFrame do
4
+ describe StackFrame do
5
5
  context "#application?" do
6
6
  it "should be true for application filenames" do
7
7
  BetterErrors.stub!(:application_root).and_return("/abc/xyz")
8
- frame = ErrorFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
8
+ frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
9
9
 
10
10
  frame.application?.should be_true
11
11
  end
12
12
 
13
13
  it "should be false for everything else" do
14
14
  BetterErrors.stub!(:application_root).and_return("/abc/xyz")
15
- frame = ErrorFrame.new("/abc/nope", 123, "foo")
15
+ frame = StackFrame.new("/abc/nope", 123, "foo")
16
16
 
17
17
  frame.application?.should be_false
18
18
  end
19
19
 
20
20
  it "should not care if no application_root is set" do
21
- frame = ErrorFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
21
+ frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
22
22
 
23
23
  frame.application?.should be_false
24
24
  end
@@ -27,14 +27,14 @@ module BetterErrors
27
27
  context "#gem?" do
28
28
  it "should be true for gem filenames" do
29
29
  Gem.stub!(:path).and_return(["/abc/xyz"])
30
- frame = ErrorFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
30
+ frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
31
31
 
32
32
  frame.gem?.should be_true
33
33
  end
34
34
 
35
35
  it "should be false for everything else" do
36
36
  Gem.stub!(:path).and_return(["/abc/xyz"])
37
- frame = ErrorFrame.new("/abc/nope", 123, "foo")
37
+ frame = StackFrame.new("/abc/nope", 123, "foo")
38
38
 
39
39
  frame.gem?.should be_false
40
40
  end
@@ -43,7 +43,7 @@ module BetterErrors
43
43
  context "#application_path" do
44
44
  it "should chop off the application root" do
45
45
  BetterErrors.stub!(:application_root).and_return("/abc/xyz")
46
- frame = ErrorFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
46
+ frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
47
47
 
48
48
  frame.application_path.should == "app/controllers/crap_controller.rb"
49
49
  end
@@ -52,7 +52,7 @@ module BetterErrors
52
52
  context "#gem_path" do
53
53
  it "should chop of the gem path and stick (gem) there" do
54
54
  Gem.stub!(:path).and_return(["/abc/xyz"])
55
- frame = ErrorFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
55
+ frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
56
56
 
57
57
  frame.gem_path.should == "(gem) whatever-1.2.3/lib/whatever.rb"
58
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_errors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-09 00:00:00.000000000 Z
12
+ date: 2012-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ! '>='
36
36
  - !ruby/object:Gem::Version
37
- version: '0'
37
+ version: 2.7.0
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
- version: '0'
45
+ version: 2.7.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: coderay
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -50,7 +50,7 @@ dependencies:
50
50
  requirements:
51
51
  - - ! '>='
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: 1.0.0
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,8 +58,10 @@ dependencies:
58
58
  requirements:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
62
- description: Better Errors gives Rails a better error page.
61
+ version: 1.0.0
62
+ description: Provides a better error page for Rails and other Rack apps. Includes
63
+ source code inspection, a live REPL and local/instance variable inspection for all
64
+ stack frames.
63
65
  email:
64
66
  - charlie@charliesomerville.com
65
67
  executables: []
@@ -73,17 +75,23 @@ files:
73
75
  - Rakefile
74
76
  - better_errors.gemspec
75
77
  - lib/better_errors.rb
78
+ - lib/better_errors/code_formatter.rb
76
79
  - lib/better_errors/core_ext/exception.rb
77
- - lib/better_errors/error_frame.rb
78
80
  - lib/better_errors/error_page.rb
79
81
  - lib/better_errors/middleware.rb
80
82
  - lib/better_errors/rails.rb
83
+ - lib/better_errors/repl.rb
84
+ - lib/better_errors/repl/basic.rb
85
+ - lib/better_errors/repl/pry.rb
86
+ - lib/better_errors/stack_frame.rb
81
87
  - lib/better_errors/templates/main.erb
82
88
  - lib/better_errors/templates/variable_info.erb
83
89
  - lib/better_errors/version.rb
84
- - spec/better_errors/error_frame_spec.rb
90
+ - spec/better_errors/code_formatter_spec.rb
85
91
  - spec/better_errors/error_page_spec.rb
86
92
  - spec/better_errors/middleware_spec.rb
93
+ - spec/better_errors/repl/basic_spec.rb
94
+ - spec/better_errors/stack_frame_spec.rb
87
95
  - spec/better_errors/support/my_source.rb
88
96
  - spec/spec_helper.rb
89
97
  homepage: https://github.com/charliesome/better_errors
@@ -110,11 +118,12 @@ rubyforge_project:
110
118
  rubygems_version: 1.8.24
111
119
  signing_key:
112
120
  specification_version: 3
113
- summary: Better Errors gives Rails a better error page
121
+ summary: Better error page for Rails and other Rack apps
114
122
  test_files:
115
- - spec/better_errors/error_frame_spec.rb
123
+ - spec/better_errors/code_formatter_spec.rb
116
124
  - spec/better_errors/error_page_spec.rb
117
125
  - spec/better_errors/middleware_spec.rb
126
+ - spec/better_errors/repl/basic_spec.rb
127
+ - spec/better_errors/stack_frame_spec.rb
118
128
  - spec/better_errors/support/my_source.rb
119
129
  - spec/spec_helper.rb
120
- has_rdoc: