ruby_method_tracer 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5fe5404bd1497820b14ca1e4bc041f778cfe9bc02402a4456329c88d0096206
4
- data.tar.gz: cb08942010ab25a16a604d0edb973bd9f889e3c34ba2ae24ead8a0dcceed4ca8
3
+ metadata.gz: 35c8ff07ff1fff38a22779065d538545ec642a043b723da76b784ee97d9a24c8
4
+ data.tar.gz: a391814be17a97f5b6820355ba21f71eb8b3ffe381589f11ddccc41f6be7c691
5
5
  SHA512:
6
- metadata.gz: f68e49cde1042577a5ed2f0054c263391070f29574d81bc58f6ec5db6f688f31411572372cce95eef010fbd6fc7cd981f38905cc01615eea914a680393af850a
7
- data.tar.gz: 54b2ccc817924cd256dfdcfb66011acfa8641b76f66d2cb0ef59dab48e32f4d2bfa9d56d56d403f5bf60b53c627d784731984e0f61fc8ad01714576eba531620
6
+ metadata.gz: a2b2c43903841a2603c5dbd2032faa11facf0c3654bb8af6b32ab88a85777e095976f4ffbfd9448efd8a9644f5be5fde4e37524ac1e4cf733fbe078b1b93ea73
7
+ data.tar.gz: c0f7126d5a885e1f4943e00651fc51ff97b614f17b86035e6fa81c083a6a74705376762f7d87a006f4e5691cdd68bc900210836a675e5ecd58d4f6e6d47f3b24
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-11-22
4
+
5
+ ### Fixed
6
+ - Fixed file permissions for `call_tree.rb`, `enhanced_tracer.rb`, and formatter files to be world-readable
7
+ - Gem now correctly includes all files when installed (previously missing EnhancedTracer and formatters)
8
+
9
+ ### Added
10
+ - Code coverage reporting with SimpleCov (99% line coverage, 84% branch coverage)
11
+ - Codecov integration for CI/CD coverage tracking
12
+ - Comprehensive test suite for BaseFormatter and TreeFormatter (18 new tests)
13
+ - Coverage badge in README
14
+
3
15
  ## [0.3.0] - 2025-11-19
4
16
 
5
17
  ### Added
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![Ruby](https://github.com/Seunadex/ruby_method_tracer/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/Seunadex/ruby_method_tracer/actions/workflows/main.yml)
2
+ [![codecov](https://codecov.io/gh/Seunadex/ruby_method_tracer/branch/main/graph/badge.svg)](https://codecov.io/gh/Seunadex/ruby_method_tracer)
2
3
 
3
4
  # RubyMethodTracer
4
5
 
@@ -253,6 +254,16 @@ After checking out the repo, run `bin/setup` to install dependencies. Then run `
253
254
 
254
255
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version in `lib/ruby_method_tracer/version.rb`, and then run `bundle exec rake release`.
255
256
 
257
+ ### Code Coverage
258
+
259
+ This project uses [SimpleCov](https://github.com/simplecov-ruby/simplecov) for code coverage analysis. Coverage reports are automatically generated when running tests:
260
+
261
+ ```bash
262
+ bundle exec rspec
263
+ ```
264
+
265
+ After running the tests, open `coverage/index.html` in your browser to view the detailed coverage report. The project maintains a minimum coverage threshold of 95% line coverage and 80% branch coverage.
266
+
256
267
  ## Contributing
257
268
 
258
269
  Bug reports and pull requests are welcome on GitHub at https://github.com/Seunadex/ruby_method_tracer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Seunadex/ruby_method_tracer/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMethodTracer
4
+ # CallTree manages the hierarchical structure of method calls,
5
+ # tracking parent-child relationships and call depths.
6
+ #
7
+ # It uses a stack-based approach to manage nested calls and builds
8
+ # a tree structure showing the complete call hierarchy.
9
+ class CallTree
10
+ attr_reader :calls, :root_calls
11
+
12
+ def initialize
13
+ @calls = [] # All recorded calls (flat list)
14
+ @call_stack = [] # Current execution stack
15
+ @root_calls = [] # Top-level calls (depth 0)
16
+ @lock = Mutex.new # Thread safety
17
+ end
18
+
19
+ # Start tracking a method call
20
+ #
21
+ # @param method_name [String] The name of the method being called
22
+ # @return [Hash] The call record that was pushed to the stack
23
+ def start_call(method_name) # rubocop:disable Metrics/MethodLength
24
+ @lock.synchronize do
25
+ call_record = {
26
+ method_name: method_name,
27
+ start_time: monotonic_time,
28
+ depth: @call_stack.size,
29
+ parent: @call_stack.last,
30
+ children: [],
31
+ status: nil,
32
+ error: nil,
33
+ execution_time: nil,
34
+ timestamp: Time.now
35
+ }
36
+
37
+ # Add as child to parent if we're nested
38
+ @call_stack.last[:children] << call_record if @call_stack.any?
39
+
40
+ # Track root-level calls
41
+ @root_calls << call_record if @call_stack.empty?
42
+
43
+ @call_stack.push(call_record)
44
+ call_record
45
+ end
46
+ end
47
+
48
+ # End tracking a method call
49
+ #
50
+ # @param status [Symbol] :success or :error
51
+ # @param error [Exception, nil] The exception if status is :error
52
+ # @return [Hash, nil] The completed call record
53
+ def end_call(status = :success, error = nil)
54
+ @lock.synchronize do
55
+ return nil if @call_stack.empty?
56
+
57
+ call_record = @call_stack.pop
58
+ call_record[:status] = status
59
+ call_record[:error] = error
60
+ call_record[:execution_time] = monotonic_time - call_record[:start_time]
61
+
62
+ @calls << call_record
63
+ call_record
64
+ end
65
+ end
66
+
67
+ # Get the current call depth
68
+ #
69
+ # @return [Integer] The current nesting level
70
+ def current_depth
71
+ @lock.synchronize { @call_stack.size }
72
+ end
73
+
74
+ # Get call hierarchy as nested structure
75
+ #
76
+ # @return [Array<Hash>] Root calls with nested children
77
+ def call_hierarchy
78
+ @lock.synchronize { @root_calls.dup }
79
+ end
80
+
81
+ # Calculate statistics from recorded calls
82
+ #
83
+ # @return [Hash] Statistics including total calls, time, slowest methods, etc.
84
+ def statistics
85
+ @lock.synchronize do
86
+ return default_statistics if @calls.empty?
87
+
88
+ method_stats = calculate_method_stats
89
+
90
+ {
91
+ total_calls: @calls.size,
92
+ total_time: @calls.sum { |c| c[:execution_time] },
93
+ unique_methods: method_stats.size,
94
+ slowest_methods: slowest_methods(method_stats),
95
+ most_called_methods: most_called_methods(method_stats),
96
+ average_time_per_method: average_times(method_stats),
97
+ max_depth: @calls.map { |c| c[:depth] }.max || 0
98
+ }
99
+ end
100
+ end
101
+
102
+ # Clear all recorded calls and reset state
103
+ def clear
104
+ @lock.synchronize do
105
+ @calls.clear
106
+ @call_stack.clear
107
+ @root_calls.clear
108
+ end
109
+ end
110
+
111
+ # Check if call stack is empty (no active calls)
112
+ #
113
+ # @return [Boolean]
114
+ def empty?
115
+ @lock.synchronize { @call_stack.empty? }
116
+ end
117
+
118
+ private
119
+
120
+ def monotonic_time
121
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
122
+ end
123
+
124
+ def default_statistics
125
+ {
126
+ total_calls: 0,
127
+ total_time: 0.0,
128
+ unique_methods: 0,
129
+ slowest_methods: [],
130
+ most_called_methods: [],
131
+ average_time_per_method: {},
132
+ max_depth: 0
133
+ }
134
+ end
135
+
136
+ def calculate_method_stats
137
+ method_stats = Hash.new { |h, k| h[k] = { calls: 0, total_time: 0.0, times: [] } }
138
+
139
+ @calls.each do |call|
140
+ stats = method_stats[call[:method_name]]
141
+ stats[:calls] += 1
142
+ stats[:total_time] += call[:execution_time]
143
+ stats[:times] << call[:execution_time]
144
+ end
145
+
146
+ method_stats
147
+ end
148
+
149
+ def slowest_methods(method_stats)
150
+ method_stats
151
+ .map { |name, stats| { method: name, avg_time: stats[:total_time] / stats[:calls] } }
152
+ .sort_by { |m| -m[:avg_time] }
153
+ .take(10)
154
+ end
155
+
156
+ def most_called_methods(method_stats)
157
+ method_stats
158
+ .map { |name, stats| { method: name, count: stats[:calls] } }
159
+ .sort_by { |m| -m[:count] }
160
+ .take(10)
161
+ end
162
+
163
+ def average_times(method_stats)
164
+ method_stats.transform_values { |stats| stats[:total_time] / stats[:calls] }
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "simple_tracer"
4
+ require_relative "call_tree"
5
+ require_relative "formatters/tree_formatter"
6
+
7
+ module RubyMethodTracer
8
+ # EnhancedTracer extends SimpleTracer with hierarchical call tracking
9
+ #
10
+ # In addition to the basic tracing functionality, this tracer maintains
11
+ # a call tree that captures parent-child relationships between method calls,
12
+ # enabling visualization of complex call hierarchies.
13
+ #
14
+ # Options:
15
+ # - All options from SimpleTracer
16
+ # - :track_hierarchy (Boolean): Enable call tree tracking; defaults to true
17
+ #
18
+ # Usage:
19
+ # tracer = RubyMethodTracer::EnhancedTracer.new(MyClass, threshold: 0.005)
20
+ # tracer.trace_method(:expensive_call)
21
+ # tracer.print_tree
22
+ class EnhancedTracer < SimpleTracer
23
+ attr_reader :call_tree
24
+
25
+ def initialize(target_class, **options)
26
+ super
27
+ @call_tree = CallTree.new
28
+ @track_hierarchy = @options.fetch(:track_hierarchy, true)
29
+ @formatter = Formatters::TreeFormatter.new
30
+ end
31
+
32
+ def trace_method(name)
33
+ method_name = name.to_sym
34
+ visibility = method_visibility(method_name)
35
+ return unless visibility
36
+ return unless mark_wrapped?(method_name)
37
+
38
+ aliased = alias_for(method_name)
39
+ @target_class.send(:alias_method, aliased, method_name)
40
+
41
+ tracer = self
42
+ key = :__ruby_method_tracer_in_trace
43
+
44
+ # Build wrapper that tracks hierarchy
45
+ @target_class.define_method(method_name, &build_enhanced_wrapper(aliased, method_name, key, tracer))
46
+
47
+ @target_class.send(visibility, method_name)
48
+ end
49
+
50
+ # Print the call tree visualization
51
+ #
52
+ # @param options [Hash] Formatting options
53
+ # @option options [Boolean] :show_errors (true) Include error information
54
+ # @option options [Boolean] :colorize (true) Apply colors to output
55
+ def print_tree(options = {})
56
+ puts @formatter.format(@call_tree, options)
57
+ end
58
+
59
+ # Get call tree as string without printing
60
+ #
61
+ # @param options [Hash] Formatting options
62
+ # @return [String] Formatted call tree
63
+ def format_tree(options = {})
64
+ @formatter.format(@call_tree, options)
65
+ end
66
+
67
+ # Get enhanced results including both flat list and hierarchy
68
+ #
69
+ # @return [Hash] Results with call tree and statistics
70
+ def fetch_enhanced_results
71
+ {
72
+ flat_calls: fetch_results,
73
+ call_hierarchy: @call_tree.call_hierarchy,
74
+ statistics: @call_tree.statistics
75
+ }
76
+ end
77
+
78
+ # Clear both simple tracer results and call tree
79
+ def clear_results
80
+ super
81
+ @call_tree.clear
82
+ end
83
+
84
+ private
85
+
86
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
87
+ def build_enhanced_wrapper(aliased, method_name, key, tracer)
88
+ track_hierarchy = tracer.instance_variable_get(:@track_hierarchy)
89
+ # Use method-specific key to prevent only SELF-recursion, not all nested calls
90
+ method_key = :"#{key}_#{method_name}"
91
+
92
+ proc do |*args, **kwargs, &block|
93
+ if track_hierarchy
94
+ # Prevent only recursive calls to the SAME method
95
+ return __send__(aliased, *args, **kwargs, &block) if Thread.current[method_key]
96
+
97
+ Thread.current[method_key] = true
98
+ full_method_name = "#{tracer.instance_variable_get(:@target_class)}##{method_name}"
99
+
100
+ # Start tracking in call tree
101
+ tracer.call_tree.start_call(full_method_name)
102
+
103
+ start = tracer.__send__(:monotonic_time)
104
+ begin
105
+ result = __send__(aliased, *args, **kwargs, &block)
106
+ execution_time = tracer.__send__(:monotonic_time) - start
107
+
108
+ # Record in both places
109
+ tracer.__send__(:record_call, method_name, execution_time, :success)
110
+ tracer.call_tree.end_call(:success)
111
+
112
+ result
113
+ rescue StandardError => e
114
+ execution_time = tracer.__send__(:monotonic_time) - start
115
+
116
+ # Record in both places
117
+ tracer.__send__(:record_call, method_name, execution_time, :error, e)
118
+ tracer.call_tree.end_call(:error, e)
119
+
120
+ raise
121
+ ensure
122
+ Thread.current[method_key] = false
123
+ end
124
+ else
125
+ tracer.__send__(:wrap_call, method_name, key) do
126
+ __send__(aliased, *args, **kwargs, &block)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
132
+
133
+ def default_options
134
+ super.merge(track_hierarchy: true)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMethodTracer
4
+ module Formatters
5
+ # Base class for formatting trace output
6
+ class BaseFormatter
7
+ # Format timing value into human-readable string
8
+ #
9
+ # @param seconds [Float] Time in seconds
10
+ # @return [String] Formatted time string
11
+ def format_time(seconds)
12
+ if seconds >= 1.0
13
+ "#{seconds.round(3)}s"
14
+ elsif seconds >= 0.001
15
+ "#{(seconds * 1000).round(1)}ms"
16
+ else
17
+ "#{(seconds * 1_000_000).round(0)}µs"
18
+ end
19
+ end
20
+
21
+ # Apply color to text using ANSI escape codes
22
+ #
23
+ # @param text [String] Text to colorize
24
+ # @param color [Symbol] Color name
25
+ # @return [String] Colorized text
26
+ def colorize(text, color)
27
+ colors = {
28
+ red: "31",
29
+ green: "32",
30
+ yellow: "33",
31
+ blue: "34",
32
+ magenta: "35",
33
+ cyan: "36",
34
+ white: "37",
35
+ reset: "0"
36
+ }
37
+ "\e[#{colors[color]}m#{text}\e[#{colors[:reset]}m"
38
+ end
39
+
40
+ # Abstract method to be implemented by subclasses
41
+ #
42
+ # @param _data [Object] Data to format
43
+ # @raise [NotImplementedError] Must be implemented by subclass
44
+ def format(_data)
45
+ raise NotImplementedError, "#{self.class} must implement #format"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_formatter"
4
+
5
+ module RubyMethodTracer
6
+ module Formatters
7
+ # TreeFormatter generates hierarchical tree visualizations of method calls
8
+ class TreeFormatter < BaseFormatter
9
+ # Format call tree into hierarchical string representation
10
+ #
11
+ # @param call_tree [CallTree] The call tree to format
12
+ # @param options [Hash] Formatting options
13
+ # @option options [Boolean] :show_errors (true) Include error information
14
+ # @option options [Boolean] :colorize (true) Apply colors to output
15
+ # @return [String] Formatted tree visualization
16
+ # rubocop:disable Metrics/AbcSize
17
+ def format(call_tree, options = {})
18
+ opts = default_options.merge(options)
19
+ root_calls = call_tree.call_hierarchy
20
+
21
+ return "No method calls recorded.\n" if root_calls.empty?
22
+
23
+ output = []
24
+ output << header
25
+ output << separator
26
+
27
+ root_calls.each_with_index do |call, index|
28
+ is_last_root = index == root_calls.size - 1
29
+ output << format_call_node(call, "", is_last_root, opts)
30
+ output << "" unless is_last_root # Blank line between root calls
31
+ end
32
+
33
+ output << separator
34
+ output << format_statistics(call_tree.statistics, opts)
35
+
36
+ output.join("\n")
37
+ end
38
+ # rubocop:enable Metrics/AbcSize
39
+
40
+ private
41
+
42
+ def default_options
43
+ {
44
+ show_errors: true,
45
+ colorize: true
46
+ }
47
+ end
48
+
49
+ def header
50
+ "METHOD CALL TREE"
51
+ end
52
+
53
+ def separator
54
+ "=" * 60
55
+ end
56
+
57
+ # Recursively format a call node and its children
58
+ #
59
+ # @param call [Hash] Call record
60
+ # @param prefix [String] Current line prefix for indentation
61
+ # @param is_last [Boolean] Whether this is the last sibling
62
+ # @param opts [Hash] Formatting options
63
+ # @return [String] Formatted call tree section
64
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
65
+ def format_call_node(call, prefix, is_last, opts)
66
+ lines = []
67
+
68
+ # Format the current call
69
+ lines << format_call_line(call, prefix, is_last, opts)
70
+
71
+ # Format error details if present
72
+ if call[:status] == :error && call[:error] && opts[:show_errors]
73
+ error_prefix = prefix + (is_last ? " " : "│ ")
74
+ lines << format_error_line(call[:error], error_prefix, opts)
75
+ end
76
+
77
+ # Format children
78
+ unless call[:children].empty?
79
+ child_prefix = prefix + (is_last ? " " : "│ ")
80
+ call[:children].each_with_index do |child, index|
81
+ is_last_child = index == call[:children].size - 1
82
+ lines << format_call_node(child, child_prefix, is_last_child, opts)
83
+ end
84
+ end
85
+
86
+ lines.join("\n")
87
+ end
88
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
89
+
90
+ # Format a single call line
91
+ #
92
+ # @param call [Hash] Call record
93
+ # @param prefix [String] Current line prefix
94
+ # @param is_last [Boolean] Whether this is the last sibling
95
+ # @param opts [Hash] Formatting options
96
+ # @return [String] Formatted call line
97
+ def format_call_line(call, prefix, is_last, opts)
98
+ connector = is_last ? "└── " : "├── "
99
+ tree_part = prefix + connector
100
+
101
+ method_name = call[:method_name]
102
+ time_str = format_time(call[:execution_time])
103
+ status_indicator = call[:status] == :error ? " [ERROR]" : ""
104
+
105
+ if opts[:colorize]
106
+ method_name = colorize(method_name, :cyan)
107
+ time_str = colorize("(#{time_str})", :yellow)
108
+ status_indicator = colorize(status_indicator, :red) unless status_indicator.empty?
109
+ else
110
+ time_str = "(#{time_str})"
111
+ end
112
+
113
+ "#{tree_part}#{method_name} #{time_str}#{status_indicator}"
114
+ end
115
+
116
+ # Format error information
117
+ #
118
+ # @param error [Exception] The error object
119
+ # @param prefix [String] Current line prefix
120
+ # @param opts [Hash] Formatting options
121
+ # @return [String] Formatted error line
122
+ def format_error_line(error, prefix, opts)
123
+ error_msg = "└─ Error: #{error.class}: #{error.message}"
124
+ error_msg = colorize(error_msg, :red) if opts[:colorize]
125
+ "#{prefix}#{error_msg}"
126
+ end
127
+
128
+ # Format statistics summary
129
+ #
130
+ # @param stats [Hash] Statistics hash from CallTree
131
+ # @param opts [Hash] Formatting options
132
+ # @return [String] Formatted statistics
133
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
134
+ def format_statistics(stats, _opts)
135
+ lines = []
136
+ lines << "\nSTATISTICS"
137
+ lines << ("-" * 60)
138
+
139
+ lines << "Total Calls: #{stats[:total_calls]}"
140
+ lines << "Total Time: #{format_time(stats[:total_time])}"
141
+ lines << "Unique Methods: #{stats[:unique_methods]}"
142
+ lines << "Max Depth: #{stats[:max_depth]}"
143
+
144
+ unless stats[:slowest_methods].empty?
145
+ lines << "\nSlowest Methods (by average time):"
146
+ stats[:slowest_methods].take(5).each_with_index do |method, index|
147
+ time_str = format_time(method[:avg_time])
148
+ lines << " #{index + 1}. #{method[:method]} - #{time_str}"
149
+ end
150
+ end
151
+
152
+ unless stats[:most_called_methods].empty?
153
+ lines << "\nMost Called Methods:"
154
+ stats[:most_called_methods].take(5).each_with_index do |method, index|
155
+ lines << " #{index + 1}. #{method[:method]} - #{method[:count]} calls"
156
+ end
157
+ end
158
+
159
+ lines.join("\n")
160
+ end
161
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
162
+ end
163
+ end
164
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyMethodTracer
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_method_tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seun Adekunle
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-19 00:00:00.000000000 Z
11
+ date: 2025-11-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A developer-friendly gem for tracing method calls, execution times, with
14
14
  minimal overhead.
@@ -26,6 +26,10 @@ files:
26
26
  - README.md
27
27
  - Rakefile
28
28
  - lib/ruby_method_tracer.rb
29
+ - lib/ruby_method_tracer/call_tree.rb
30
+ - lib/ruby_method_tracer/enhanced_tracer.rb
31
+ - lib/ruby_method_tracer/formatters/base_formatter.rb
32
+ - lib/ruby_method_tracer/formatters/tree_formatter.rb
29
33
  - lib/ruby_method_tracer/simple_tracer.rb
30
34
  - lib/ruby_method_tracer/version.rb
31
35
  - sig/ruby_method_tracer.rbs