tp_tree 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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +145 -0
- data/Rakefile +12 -0
- data/examples/interactive_timing_demo.rb +35 -0
- data/examples/semi_empty_nodes_demo.rb +47 -0
- data/examples/timing_demo.rb +28 -0
- data/exe/tp_tree +23 -0
- data/lib/tp_tree/call_stack.rb +72 -0
- data/lib/tp_tree/formatter.rb +47 -0
- data/lib/tp_tree/formatters/ansi_formatter.rb +20 -0
- data/lib/tp_tree/formatters/base_formatter.rb +87 -0
- data/lib/tp_tree/formatters/xml_formatter.rb +14 -0
- data/lib/tp_tree/interactive_viewer.rb +687 -0
- data/lib/tp_tree/method_filter.rb +53 -0
- data/lib/tp_tree/presenters/tree_node_presenter.rb +76 -0
- data/lib/tp_tree/tree_builder.rb +136 -0
- data/lib/tp_tree/tree_node.rb +105 -0
- data/lib/tp_tree/version.rb +5 -0
- data/lib/tp_tree/xml_formatter.rb +47 -0
- data/lib/tp_tree.rb +38 -0
- data/sig/tp_tree.rbs +4 -0
- metadata +71 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TPTree
|
4
|
+
# MethodFilter handles filtering and excluding method calls based on various criteria
|
5
|
+
class MethodFilter
|
6
|
+
def initialize(filter: nil, exclude: nil)
|
7
|
+
@filter_matchers = build_matchers(filter) if filter
|
8
|
+
@exclude_matchers = build_matchers(exclude) if exclude
|
9
|
+
end
|
10
|
+
|
11
|
+
def should_include?(method_name, defined_class, tp)
|
12
|
+
# If we have filters, method must match at least one filter
|
13
|
+
if @filter_matchers && !@filter_matchers.empty?
|
14
|
+
return false unless matches_any?(@filter_matchers, method_name, defined_class, tp)
|
15
|
+
end
|
16
|
+
|
17
|
+
# If we have excludes, method must not match any exclude
|
18
|
+
if @exclude_matchers && !@exclude_matchers.empty?
|
19
|
+
return false if matches_any?(@exclude_matchers, method_name, defined_class, tp)
|
20
|
+
end
|
21
|
+
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def build_matchers(criteria)
|
28
|
+
case criteria
|
29
|
+
when Array
|
30
|
+
criteria.map { |item| build_single_matcher(item) }
|
31
|
+
else
|
32
|
+
[build_single_matcher(criteria)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_single_matcher(criterion)
|
37
|
+
case criterion
|
38
|
+
when String
|
39
|
+
->(method_name, defined_class, tp) { method_name.to_s == criterion }
|
40
|
+
when Regexp
|
41
|
+
->(method_name, defined_class, tp) { criterion.match?(method_name.to_s) }
|
42
|
+
when Proc
|
43
|
+
criterion
|
44
|
+
else
|
45
|
+
raise ArgumentError, "Filter/exclude criteria must be String, Regexp, Array, or Proc"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def matches_any?(matchers, method_name, defined_class, tp)
|
50
|
+
matchers.any? { |matcher| matcher.call(method_name, defined_class, tp) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../formatters/ansi_formatter'
|
4
|
+
|
5
|
+
module TPTree
|
6
|
+
module Presenters
|
7
|
+
# TreeNodePresenter handles the formatting and display logic for TreeNode objects
|
8
|
+
class TreeNodePresenter
|
9
|
+
def initialize(tree_node, formatter: nil)
|
10
|
+
@tree_node = tree_node
|
11
|
+
@formatter = formatter || Formatters::AnsiFormatter.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
prefix = build_prefix
|
16
|
+
color = @formatter.color_for_depth(@tree_node.depth)
|
17
|
+
colored_method_name = @formatter.colorize(@tree_node.method_name, color)
|
18
|
+
timing_info = @formatter.format_timing(@tree_node.duration)
|
19
|
+
|
20
|
+
case @tree_node.event
|
21
|
+
when :call
|
22
|
+
"#{prefix}#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)})#{timing_info}"
|
23
|
+
when :return
|
24
|
+
"#{prefix}#{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
|
25
|
+
when :call_return
|
26
|
+
"#{prefix}#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)}) → #{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_parts
|
31
|
+
prefix_parts = build_prefix_parts
|
32
|
+
color = @formatter.color_for_depth(@tree_node.depth)
|
33
|
+
colored_method_name = @formatter.colorize(@tree_node.method_name, color)
|
34
|
+
timing_info = @formatter.format_timing(@tree_node.duration)
|
35
|
+
|
36
|
+
content = case @tree_node.event
|
37
|
+
when :call
|
38
|
+
"#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)})#{timing_info}"
|
39
|
+
when :return
|
40
|
+
"#{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
|
41
|
+
when :call_return
|
42
|
+
"#{colored_method_name}(#{@formatter.format_parameters(@tree_node.parameters)}) → #{@formatter.format_return_value(@tree_node.return_value)}#{timing_info}"
|
43
|
+
end
|
44
|
+
[prefix_parts, content]
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def build_prefix_parts
|
50
|
+
parts = []
|
51
|
+
color = @formatter.color_for_depth(@tree_node.depth)
|
52
|
+
|
53
|
+
if @tree_node.event == :return
|
54
|
+
(0...@tree_node.depth).each do |level|
|
55
|
+
parts << ['│ ', @formatter.color_for_depth(level)]
|
56
|
+
end
|
57
|
+
parts << ['└→ ', color]
|
58
|
+
return parts
|
59
|
+
end
|
60
|
+
|
61
|
+
return [] if @tree_node.depth.zero?
|
62
|
+
|
63
|
+
(0...@tree_node.depth).each do |level|
|
64
|
+
parts << ['│ ', @formatter.color_for_depth(level)]
|
65
|
+
end
|
66
|
+
parts
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_prefix
|
70
|
+
build_prefix_parts.map do |text, color|
|
71
|
+
@formatter.colorize(text, color)
|
72
|
+
end.join
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'tree_node'
|
4
|
+
require_relative 'call_stack'
|
5
|
+
|
6
|
+
module TPTree
|
7
|
+
# TreeBuilder uses TracePoint to build a tree of method calls.
|
8
|
+
class TreeBuilder
|
9
|
+
attr_reader :events
|
10
|
+
|
11
|
+
def initialize(method_filter: nil, &block)
|
12
|
+
@call_stack = CallStack.new
|
13
|
+
@method_filter = method_filter
|
14
|
+
@block = block
|
15
|
+
end
|
16
|
+
|
17
|
+
def events
|
18
|
+
@call_stack.events
|
19
|
+
end
|
20
|
+
|
21
|
+
def build
|
22
|
+
tp = TracePoint.trace(:call, :return) do |tp|
|
23
|
+
next if tp.callee_id == :disable && tp.defined_class == TracePoint
|
24
|
+
|
25
|
+
case tp.event
|
26
|
+
when :call
|
27
|
+
handle_call(tp)
|
28
|
+
when :return
|
29
|
+
handle_return(tp)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@block.call
|
34
|
+
tp.disable
|
35
|
+
|
36
|
+
events
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def handle_call(tp)
|
42
|
+
# Apply filtering if a method filter is configured
|
43
|
+
if @method_filter && !@method_filter.should_include?(tp.callee_id, tp.defined_class, tp)
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
param_values = extract_parameters(tp)
|
48
|
+
call_time = Time.now
|
49
|
+
|
50
|
+
@call_stack.start_call(
|
51
|
+
tp.callee_id,
|
52
|
+
param_values,
|
53
|
+
tp.defined_class,
|
54
|
+
tp.path,
|
55
|
+
tp.lineno,
|
56
|
+
call_time
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_return(tp)
|
61
|
+
return_time = Time.now
|
62
|
+
call_info = @call_stack.finish_call(tp.callee_id, tp.return_value, return_time)
|
63
|
+
|
64
|
+
# If call_info is nil, the method was filtered out during call
|
65
|
+
return unless call_info
|
66
|
+
|
67
|
+
if call_info[:has_children]
|
68
|
+
# Create separate call and return events
|
69
|
+
call_node = TreeNode.new(
|
70
|
+
:call,
|
71
|
+
call_info[:method_name],
|
72
|
+
call_info[:parameters],
|
73
|
+
nil,
|
74
|
+
call_info[:depth],
|
75
|
+
call_info[:defined_class],
|
76
|
+
call_info[:path],
|
77
|
+
call_info[:lineno],
|
78
|
+
call_info[:start_time],
|
79
|
+
return_time
|
80
|
+
)
|
81
|
+
|
82
|
+
return_node = TreeNode.new(
|
83
|
+
:return,
|
84
|
+
tp.callee_id,
|
85
|
+
nil,
|
86
|
+
tp.return_value,
|
87
|
+
@call_stack.current_depth,
|
88
|
+
tp.defined_class,
|
89
|
+
tp.path,
|
90
|
+
tp.lineno,
|
91
|
+
call_info[:start_time],
|
92
|
+
return_time
|
93
|
+
)
|
94
|
+
|
95
|
+
# Replace placeholder and add return event
|
96
|
+
@call_stack.set_event_at_index(call_info[:event_index], call_node)
|
97
|
+
@call_stack.add_event(return_node)
|
98
|
+
else
|
99
|
+
# Create single call_return event
|
100
|
+
call_return_node = TreeNode.new(
|
101
|
+
:call_return,
|
102
|
+
call_info[:method_name],
|
103
|
+
call_info[:parameters],
|
104
|
+
tp.return_value,
|
105
|
+
call_info[:depth],
|
106
|
+
call_info[:defined_class],
|
107
|
+
call_info[:path],
|
108
|
+
call_info[:lineno],
|
109
|
+
call_info[:start_time],
|
110
|
+
return_time
|
111
|
+
)
|
112
|
+
|
113
|
+
# Replace placeholder
|
114
|
+
@call_stack.set_event_at_index(call_info[:event_index], call_return_node)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def extract_parameters(tp)
|
119
|
+
return tp.parameters.map { |type, name| [type, name, nil] } unless tp.binding
|
120
|
+
|
121
|
+
tp.parameters.map do |param_type, param_name|
|
122
|
+
begin
|
123
|
+
value = case param_type
|
124
|
+
when :req, :opt, :keyreq, :key
|
125
|
+
tp.binding.local_variable_get(param_name)
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
[param_type, param_name, value]
|
130
|
+
rescue NameError
|
131
|
+
[param_type, param_name, nil]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'formatter'
|
4
|
+
require_relative 'presenters/tree_node_presenter'
|
5
|
+
|
6
|
+
module TPTree
|
7
|
+
# TreeNode represents a single event in the method call tree
|
8
|
+
class TreeNode
|
9
|
+
include Formatter
|
10
|
+
|
11
|
+
attr_reader :event, :method_name, :parameters, :return_value, :depth, :defined_class, :path, :lineno, :start_time, :end_time
|
12
|
+
|
13
|
+
def initialize(event, method_name, parameters = nil, return_value = nil, depth = 0, defined_class = nil, path = nil, lineno = nil, start_time = nil, end_time = nil)
|
14
|
+
@event = event
|
15
|
+
@method_name = method_name
|
16
|
+
@parameters = parameters
|
17
|
+
@return_value = return_value
|
18
|
+
@depth = depth
|
19
|
+
@defined_class = defined_class
|
20
|
+
@path = path
|
21
|
+
@lineno = lineno
|
22
|
+
@start_time = start_time
|
23
|
+
@end_time = end_time
|
24
|
+
end
|
25
|
+
|
26
|
+
def duration
|
27
|
+
return nil unless @start_time && @end_time
|
28
|
+
@end_time - @start_time
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
{
|
33
|
+
event: @event,
|
34
|
+
method_name: @method_name,
|
35
|
+
parameters: serialize_parameters(@parameters),
|
36
|
+
return_value: serialize_value(@return_value),
|
37
|
+
depth: @depth,
|
38
|
+
defined_class: @defined_class&.to_s,
|
39
|
+
path: @path,
|
40
|
+
lineno: @lineno,
|
41
|
+
start_time: @start_time&.to_f,
|
42
|
+
end_time: @end_time&.to_f,
|
43
|
+
duration: duration
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s(formatter: self)
|
48
|
+
# Create appropriate formatter for presenter
|
49
|
+
presenter_formatter = if formatter.respond_to?(:formatter)
|
50
|
+
formatter.formatter
|
51
|
+
elsif formatter.respond_to?(:colorize)
|
52
|
+
formatter
|
53
|
+
else
|
54
|
+
# Fallback to default ANSI formatter
|
55
|
+
Formatters::AnsiFormatter.new
|
56
|
+
end
|
57
|
+
|
58
|
+
presenter = Presenters::TreeNodePresenter.new(self, formatter: presenter_formatter)
|
59
|
+
presenter.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_parts(formatter: self)
|
63
|
+
# Create appropriate formatter for presenter
|
64
|
+
presenter_formatter = if formatter.respond_to?(:formatter)
|
65
|
+
formatter.formatter
|
66
|
+
elsif formatter.respond_to?(:colorize)
|
67
|
+
formatter
|
68
|
+
else
|
69
|
+
# Fallback to default ANSI formatter
|
70
|
+
Formatters::AnsiFormatter.new
|
71
|
+
end
|
72
|
+
|
73
|
+
presenter = Presenters::TreeNodePresenter.new(self, formatter: presenter_formatter)
|
74
|
+
presenter.to_parts
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def serialize_parameters(parameters)
|
80
|
+
return nil unless parameters
|
81
|
+
parameters.map { |param_type, param_name, param_value|
|
82
|
+
{
|
83
|
+
type: param_type,
|
84
|
+
name: param_name,
|
85
|
+
value: serialize_value(param_value)
|
86
|
+
}
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def serialize_value(value)
|
91
|
+
case value
|
92
|
+
when String, Symbol, NilClass, TrueClass, FalseClass, Numeric
|
93
|
+
value
|
94
|
+
when Array
|
95
|
+
value.map { |v| serialize_value(v) }
|
96
|
+
when Hash
|
97
|
+
value.transform_values { |v| serialize_value(v) }
|
98
|
+
when Proc
|
99
|
+
'Proc'
|
100
|
+
else
|
101
|
+
value.inspect
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'formatters/xml_formatter'
|
4
|
+
|
5
|
+
module TPTree
|
6
|
+
# XMLFormatter provides methods for colorizing and formatting output using XML tags.
|
7
|
+
# This module acts as a compatibility layer for the old XMLFormatter module.
|
8
|
+
module XMLFormatter
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(FormatterMethods)
|
11
|
+
base.include(FormatterMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module FormatterMethods
|
15
|
+
def formatter
|
16
|
+
@xml_formatter ||= Formatters::XmlFormatter.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def colorize(text, color)
|
20
|
+
formatter.colorize(text, color)
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_timing(duration)
|
24
|
+
formatter.format_timing(duration)
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_parameters(parameters)
|
28
|
+
formatter.format_parameters(parameters)
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_value(value)
|
32
|
+
formatter.format_value(value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_return_value(return_value)
|
36
|
+
formatter.format_return_value(return_value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def color_for_depth(depth)
|
40
|
+
formatter.color_for_depth(depth)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Expose constants for backward compatibility
|
45
|
+
DEPTH_COLORS = Formatters::BaseFormatter::DEPTH_COLORS
|
46
|
+
end
|
47
|
+
end
|
data/lib/tp_tree.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tp_tree/version"
|
4
|
+
require_relative "tp_tree/tree_builder"
|
5
|
+
require_relative "tp_tree/method_filter"
|
6
|
+
|
7
|
+
module TPTree
|
8
|
+
class <<self
|
9
|
+
# catch sets up a TracePoint to monitor method calls and returns,
|
10
|
+
# printing them in chronological order with proper tree indentation.
|
11
|
+
#
|
12
|
+
# @param interactive [Boolean] whether to show interactive viewer
|
13
|
+
# @param write_to [String] file path to write JSON output to
|
14
|
+
# @param filter [String, Regexp, Array, Proc] only include methods matching these criteria
|
15
|
+
# @param exclude [String, Regexp, Array, Proc] exclude methods matching these criteria
|
16
|
+
def catch(interactive: false, write_to: nil, filter: nil, exclude: nil, &block)
|
17
|
+
filter_obj = MethodFilter.new(filter: filter, exclude: exclude) if filter || exclude
|
18
|
+
events = TreeBuilder.new(method_filter: filter_obj, &block).build
|
19
|
+
|
20
|
+
if interactive
|
21
|
+
require_relative "tp_tree/interactive_viewer"
|
22
|
+
InteractiveViewer.new(events).show
|
23
|
+
elsif write_to
|
24
|
+
require 'json'
|
25
|
+
require 'time'
|
26
|
+
json_data = {
|
27
|
+
version: TPTree::VERSION,
|
28
|
+
timestamp: Time.now.strftime('%Y-%m-%dT%H:%M:%S%z'),
|
29
|
+
events: events.map(&:to_hash)
|
30
|
+
}
|
31
|
+
File.write(write_to, JSON.pretty_generate(json_data))
|
32
|
+
puts "Trace data written to: #{write_to}"
|
33
|
+
else
|
34
|
+
events.each { |event| puts event }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/sig/tp_tree.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tp_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dmytro Koval
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: TPTree provides a simple but powerful tracer for method calls, using
|
13
|
+
TracePoint.
|
14
|
+
email:
|
15
|
+
- contact@dkoval.pro
|
16
|
+
executables:
|
17
|
+
- tp_tree
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".rspec"
|
22
|
+
- ".rubocop.yml"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- CODE_OF_CONDUCT.md
|
25
|
+
- LICENSE.txt
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- examples/interactive_timing_demo.rb
|
29
|
+
- examples/semi_empty_nodes_demo.rb
|
30
|
+
- examples/timing_demo.rb
|
31
|
+
- exe/tp_tree
|
32
|
+
- lib/tp_tree.rb
|
33
|
+
- lib/tp_tree/call_stack.rb
|
34
|
+
- lib/tp_tree/formatter.rb
|
35
|
+
- lib/tp_tree/formatters/ansi_formatter.rb
|
36
|
+
- lib/tp_tree/formatters/base_formatter.rb
|
37
|
+
- lib/tp_tree/formatters/xml_formatter.rb
|
38
|
+
- lib/tp_tree/interactive_viewer.rb
|
39
|
+
- lib/tp_tree/method_filter.rb
|
40
|
+
- lib/tp_tree/presenters/tree_node_presenter.rb
|
41
|
+
- lib/tp_tree/tree_builder.rb
|
42
|
+
- lib/tp_tree/tree_node.rb
|
43
|
+
- lib/tp_tree/version.rb
|
44
|
+
- lib/tp_tree/xml_formatter.rb
|
45
|
+
- sig/tp_tree.rbs
|
46
|
+
homepage: https://github.com/dmk/tp_tree
|
47
|
+
licenses:
|
48
|
+
- MIT
|
49
|
+
metadata:
|
50
|
+
allowed_push_host: https://rubygems.org
|
51
|
+
homepage_uri: https://github.com/dmk/tp_tree
|
52
|
+
source_code_uri: https://github.com/dmk/tp_tree
|
53
|
+
changelog_uri: https://github.com/dmk/tp_tree/blob/main/CHANGELOG.md
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.1.0
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.6.9
|
69
|
+
specification_version: 4
|
70
|
+
summary: A simple tracer for method calls.
|
71
|
+
test_files: []
|