Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Better RSpec output (WIP) #56

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/pry-stack_explorer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "pry" unless defined?(::Pry)
require "pry-stack_explorer/version"
require "pry-stack_explorer/commands"
require "pry-stack_explorer/frame"
require "pry-stack_explorer/frame_manager"
require "pry-stack_explorer/when_started_hook"
require "binding_of_caller"
Expand Down
96 changes: 18 additions & 78 deletions lib/pry-stack_explorer/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,69 +20,6 @@ def prior_context_exists?
frame_managers.count > 1 || frame_manager.prior_binding
end

# Return a description of the frame (binding).
# This is only useful for regular old bindings that have not been
# enhanced by `#of_caller`.
# @param [Binding] b The binding.
# @return [String] A description of the frame (binding).
def frame_description(b)
b_self = b.eval('self')
b_method = b.eval('__method__')

if b_method && b_method != :__binding__ && b_method != :__binding_impl__
b_method.to_s
elsif b_self.instance_of?(Module)
"<module:#{b_self}>"
elsif b_self.instance_of?(Class)
"<class:#{b_self}>"
else
"<main>"
end
end

# Return a description of the passed binding object. Accepts an
# optional `verbose` parameter.
# @param [Binding] b The binding.
# @param [Boolean] verbose Whether to generate a verbose description.
# @return [String] The description of the binding.
def frame_info(b, verbose = false)
meth = b.eval('__method__')
b_self = b.eval('self')
meth_obj = Pry::Method.from_binding(b) if meth

type = b.frame_type ? "[#{b.frame_type}]".ljust(9) : ""
desc = b.frame_description ? "#{b.frame_description}" : "#{frame_description(b)}"
sig = meth_obj ? "<#{signature_with_owner(meth_obj)}>" : ""

self_clipped = "#{Pry.view_clip(b_self)}"
path = '@ ' + b.source_location.join(':')

if !verbose
"#{type} #{desc} #{sig}"
else
"#{type} #{desc} #{sig}\n in #{self_clipped} #{path}"
end
end

# @param [Pry::Method] meth_obj The method object.
# @return [String] Signature for the method object in Class#method format.
def signature_with_owner(meth_obj)
if !meth_obj.undefined?
args = meth_obj.parameters.inject([]) do |arr, (type, name)|
name ||= (type == :block ? 'block' : "arg#{arr.size + 1}")
arr << case type
when :req then name.to_s
when :opt then "#{name}=?"
when :rest then "*#{name}"
when :block then "&#{name}"
else '?'
end
end
"#{meth_obj.name_with_owner}(#{args.join(', ')})"
else
"#{meth_obj.name_with_owner}(UNKNOWN) (undefined method)"
end
end

# Regexp.new(args[0])
def find_frame_by_regex(regex, up_or_down)
Expand Down Expand Up @@ -225,7 +162,8 @@ def process
new_frame_index = find_frame_by_regex(Regexp.new(args[0]), :up)
frame_manager.change_frame_to new_frame_index
else
output.puts "##{frame_manager.binding_index} #{frame_info(target, true)}"
frame = PryStackExplorer::Frame.make(target)
output.puts "##{frame_manager.binding_index} #{frame.info(verbose: true)}"
end
end
end
Expand All @@ -250,18 +188,6 @@ def options(opt)
opt.on :a, :app, "Display application frames only", optional_argument: true
end

def memoized_info(index, b, verbose)
frame_manager.user[:frame_info] ||= Hash.new { |h, k| h[k] = [] }

if verbose
frame_manager.user[:frame_info][:v][index] ||= frame_info(b, verbose)
else
frame_manager.user[:frame_info][:normal][index] ||= frame_info(b, verbose)
end
end

private :memoized_info

# @return [Array<Fixnum, Array<Binding>>] Return tuple of
# base_frame_index and the array of frames.
def selected_stack_frames
Expand Down Expand Up @@ -320,11 +246,25 @@ def frames_with_indices
end
end

ARROW = "=>"
EMPTY = " "

# "=> #0 method_name <Class#method(...)>"
def make_stack_line(b, i, active)
arw = active ? "=>" : " "
arrow = active ? ARROW : EMPTY
frame_no = i.to_s.rjust(2)
frame_info = memoized_frame(i, b).info(verbose: opts[:v])

[
arrow,
blue(bold frame_no) + ":",
frame_info,
].join(" ")
end

"#{arw} ##{i} #{memoized_info(i, b, opts[:v])}"
def memoized_frame(index, b)
frame_manager.user[:frame_info] ||= {}
frame_manager.user[:frame_info][index] ||= PryStackExplorer::Frame.make(b)
end

def offset_frames
Expand Down
186 changes: 186 additions & 0 deletions lib/pry-stack_explorer/frame.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
require_relative "frame/it_block"

module PryStackExplorer
class Frame
attr_reader :b

def self.make(_binding)
if defined?(RSpec::Core) && _binding.receiver.is_a?(RSpec::Core::ExampleGroup)
ItBlock.new(_binding)
else
new(_binding)
end
end

def initialize(_binding)
@b = _binding
end

# Return a description of the frame (binding).
# This is only useful for regular old bindings that have not been
# enhanced by `#of_caller`.
# @return [String] A description of the frame (binding).
def _description
return b.frame_description if b.frame_description

if is_method?
_method.to_s
elsif b.receiver.instance_of?(Module)
"<module:#{b.receiver}>"
elsif b.receiver.instance_of?(Class)
"<class:#{b.receiver}>"
else
"<main>"
end
end

DESCRIPTION_PATTERN = %r{
(?<context>
(?:
block\s
(?:\(\d\ levels\)\ )?
)
(?:in\ )
)?
(?<method>.*)
}x

def description
return unless _description
_description.match(DESCRIPTION_PATTERN).named_captures
end

module T
extend Pry::Helpers::Text

# Not in Pry yet
def self.faded(text)
"\e[2m#{text}\e[0m"
end
end

COLOR_SCHEME = {
description: {
context: :default,
method: :green,
},
signature: {
module: :default,
method: [:blue, :bold],
arguments: :blue,
}
}

# faded(" | ")
PIPE = "\e[2m | \e[0m"

def apply_color(string, color = nil, weight = nil)
return unless string

string = T.public_send(weight, string) if weight
string = T.public_send(color, string) if color

string
end

# Produces a string describing the frame
# @param [Options] verbose: Whether to generate a verbose description.
# @return [String] The description of the binding.
def info(verbose: false)
return @info[!!verbose] if @info

@info = _info
@info[!!verbose]
end

def _info
output = {}
output[:type] = T.faded(type.to_s.ljust(10))

output[:full_description] = [
# description
[
apply_color(description["context"], nil),
apply_color(description["method"], :green),
].compact.join(""),

# signature
[
apply_color(signature['module'], nil),
apply_color(signature['method'], :blue, :bold),
apply_color(signature['arguments'], :faded),
].compact.join("")

].compact.join(PIPE)

base = output.values.join("")

extra_info = " in #{self_clipped} #{path}"

{
false => base,
true => base + "\n" + extra_info,
}
end

def type
b.frame_type
end

def _method
@_method ||= b.eval('__method__')
end

def is_method?
_method &&
_method != :__binding__ &&
_method != :__binding_impl__
end

def self_clipped
Pry.view_clip(b.receiver)
end

def path
'@ ' + b.source_location.join(':')
end

def pry_method
Pry::Method.from_binding(b) if _method
end

SIGNATURE_PATTERN = /
(?<module>.*?)
(?<method>[#\.].*?)
(?<arguments>\(.*?\))
/x

def signature
return {} unless pry_method
string = self.class.method_signature_with_owner(pry_method)

# Will match strings like `Module::Module#method(args)`
string.match(SIGNATURE_PATTERN).named_captures
end

# @param [Pry::Method] pry_method The method object.
# @return [String] Signature for the method object in Class#method format.
def self.method_signature_with_owner(pry_method)
if pry_method.undefined?
return "#{pry_method.name_with_owner}(UNKNOWN) (undefined method)"
end

args = pry_method.parameters.inject([]) do |arr, (type, name)|
name ||= (type == :block ? 'block' : "arg#{arr.size + 1}")
arr << case type
when :req then name.to_s
when :opt then "#{name}=?"
when :rest then "*#{name}"
when :block then "&#{name}"
else '?'
end
end
"#{pry_method.name_with_owner}(#{args.join(', ')})"
end
end
end
59 changes: 59 additions & 0 deletions lib/pry-stack_explorer/frame/it_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module PryStackExplorer
class Frame
class ItBlock < self
def description
if is_method?
pry_method.name
else
it_description
end
end

# it "does fun things"
def it_description
if metadata[:location]
"it " + metadata[:description]&.inspect
else
"it (anonymous)"
end
end

def sig
super || metadata[:class_name]
end

def type
if is_method?
super || "block"
else
"it"
end
end

# Matches:
# #<RSpec::ExampleGroups::ThatClass::Fun "does the fun" (./test/rspec_spec.rb:9)>
# #<RSpec::ExampleGroups::ThatClass::Fun "example at ./test/rspec_spec.rb:12">
INSPECT_REGEXP = %r{
\#<
(?<class_name>.+?)
(
\s"
(?<description>.*?)
"
)?
(
\s\(
(?<location>.*?)
\)
)?
>
}x

def metadata
@metadata ||= b.receiver.inspect
.match(INSPECT_REGEXP)
.named_captures.transform_keys(&:to_sym)
end
end
end
end