Skip to content

Commit f71f485

Browse files
committed
Refactor: Introduce Codetracer::KernelPatches module (#68)
* Refactor: Introduce Codetracer::KernelPatches module This commit introduces a new module `Codetracer::KernelPatches` to centralize the monkey-patching logic for `Kernel#p`, `Kernel#puts`, and `Kernel#print`. Key changes: - Created `codetracer/kernel_patches.rb` for the new module. - The `Codetracer::KernelPatches` module now manages a list of active tracers. - Kernel methods are patched only once. Events are dispatched to all registered tracers. - `install(tracer)` and `uninstall(tracer)` methods allow tracers to register and unregister. - Original kernel methods are restored when the last tracer is uninstalled. - Event recording consistently uses `caller_locations(1,1).first` for path and line info. - Both the pure Ruby tracer (`trace.rb`) and the native tracer (`native_trace.rb`) have been updated to use this shared module. - Added comprehensive Minitest unit tests for `Codetracer::KernelPatches` covering various scenarios including multiple tracers and method restoration. This refactoring improves modularity, simplifies maintenance, and enables multiple tracer instances to coexist and receive kernel events simultaneously.
1 parent 31c0063 commit f71f485

File tree

5 files changed

+223
-72
lines changed

5 files changed

+223
-72
lines changed

Justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
alias t := test
22

33
test:
4-
ruby -Itest test/test_tracer.rb
4+
ruby -Itest test/test_kernel_patches.rb test/test_tracer.rb
55

66
bench pattern="*" write_report="console":
77
ruby test/benchmarks/run_benchmarks.rb '{{pattern}}' --write-report={{write_report}}

codetracer/kernel_patches.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-License-Identifier: MIT
2+
3+
module Codetracer
4+
module KernelPatches
5+
@@tracers = []
6+
@@original_methods = {}
7+
8+
def self.install(tracer)
9+
@@tracers << tracer
10+
11+
if @@original_methods.empty?
12+
@@original_methods[:p] = Kernel.instance_method(:p)
13+
@@original_methods[:puts] = Kernel.instance_method(:puts)
14+
@@original_methods[:print] = Kernel.instance_method(:print)
15+
16+
Kernel.module_eval do
17+
alias_method :old_p, :p unless method_defined?(:old_p)
18+
alias_method :old_puts, :puts unless method_defined?(:old_puts)
19+
alias_method :old_print, :print unless method_defined?(:old_print)
20+
21+
define_method(:p) do |*args|
22+
loc = caller_locations(1,1).first
23+
@@tracers.each do |t|
24+
t.record_event(loc.path, loc.lineno, args.map(&:inspect).join("\n"))
25+
end
26+
@@original_methods[:p].bind(self).call(*args)
27+
end
28+
29+
define_method(:puts) do |*args|
30+
loc = caller_locations(1,1).first
31+
@@tracers.each do |t|
32+
t.record_event(loc.path, loc.lineno, args.join("\n"))
33+
end
34+
@@original_methods[:puts].bind(self).call(*args)
35+
end
36+
37+
define_method(:print) do |*args|
38+
loc = caller_locations(1,1).first
39+
@@tracers.each do |t|
40+
t.record_event(loc.path, loc.lineno, args.join)
41+
end
42+
@@original_methods[:print].bind(self).call(*args)
43+
end
44+
end
45+
end
46+
end
47+
48+
def self.uninstall(tracer)
49+
@@tracers.delete(tracer)
50+
51+
if @@tracers.empty? && !@@original_methods.empty?
52+
Kernel.module_eval do
53+
define_method(:p, @@original_methods[:p])
54+
define_method(:puts, @@original_methods[:puts])
55+
define_method(:print, @@original_methods[:print])
56+
end
57+
@@original_methods.clear
58+
end
59+
end
60+
end
61+
end

gems/codetracer-pure-ruby-recorder/lib/trace.rb

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'json'
66
require 'optparse'
77
require_relative 'recorder'
8+
require_relative '../../../codetracer/kernel_patches'
89

910

1011
# Warning:
@@ -18,46 +19,6 @@
1819
# however this seems as a risky solution, as it clears global gem state!
1920
# BE CAREFUL if you have other ruby projects/data there!
2021

21-
# instrumentation helpers for recording IO calls
22-
module CodetracerKernelPatches
23-
def self.install(tracer)
24-
Kernel.module_eval do
25-
unless method_defined?(:old_p)
26-
alias :old_p :p
27-
alias :old_puts :puts
28-
alias :old_print :print
29-
end
30-
31-
define_method(:p) do |*args|
32-
if tracer.tracing
33-
tracer.deactivate
34-
tracer.record_event(caller, args.join("\n"))
35-
tracer.activate
36-
end
37-
old_p(*args)
38-
end
39-
40-
define_method(:puts) do |*args|
41-
if tracer.tracing
42-
tracer.deactivate
43-
tracer.record_event(caller, args.join("\n"))
44-
tracer.activate
45-
end
46-
old_puts(*args)
47-
end
48-
49-
define_method(:print) do |*args|
50-
if tracer.tracing
51-
tracer.deactivate
52-
tracer.record_event(caller, args.join("\n"))
53-
tracer.activate
54-
end
55-
old_print(*args)
56-
end
57-
end
58-
end
59-
end
60-
6122
# class IO
6223
# alias :old_write :write
6324

@@ -257,7 +218,7 @@ def load_variables(binding)
257218

258219
if __FILE__ == $PROGRAM_NAME
259220
$tracer = Tracer.new($codetracer_record, debug: ENV['CODETRACER_RUBY_RECORDER_DEBUG'] == '1')
260-
CodetracerKernelPatches.install($tracer)
221+
::Codetracer::KernelPatches.install($tracer)
261222

262223
options = {}
263224
parser = OptionParser.new do |opts|
@@ -286,6 +247,9 @@ def load_variables(binding)
286247
$tracer.ignore('gems/')
287248

288249
$tracer.activate
250+
at_exit do
251+
::Codetracer::KernelPatches.uninstall($tracer)
252+
end
289253
begin
290254
Kernel.load(program)
291255
rescue Exception => e

gems/codetracer-ruby-recorder/lib/native_trace.rb

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'optparse'
66
require 'fileutils'
77
require 'rbconfig'
8+
require_relative '../../../codetracer/kernel_patches'
89

910
options = {}
1011
parser = OptionParser.new do |opts|
@@ -50,36 +51,7 @@
5051
require target_path
5152
recorder = RubyRecorder.new
5253
$recorder = recorder
53-
54-
module Kernel
55-
alias :old_p :p
56-
alias :old_puts :puts
57-
alias :old_print :print
58-
59-
def p(*args)
60-
if $recorder
61-
loc = caller_locations(1,1).first
62-
$recorder.record_event(loc.path, loc.lineno, args.join("\n"))
63-
end
64-
old_p(*args)
65-
end
66-
67-
def puts(*args)
68-
if $recorder
69-
loc = caller_locations(1,1).first
70-
$recorder.record_event(loc.path, loc.lineno, args.join("\n"))
71-
end
72-
old_puts(*args)
73-
end
74-
75-
def print(*args)
76-
if $recorder
77-
loc = caller_locations(1,1).first
78-
$recorder.record_event(loc.path, loc.lineno, args.join("\n"))
79-
end
80-
old_print(*args)
81-
end
82-
end
54+
::Codetracer::KernelPatches.install(recorder)
8355

8456
rescue Exception => e
8557
warn "native tracer unavailable: #{e}"
@@ -90,6 +62,7 @@ def print(*args)
9062
load program
9163
if recorder
9264
recorder.disable_tracing
65+
::Codetracer::KernelPatches.uninstall(recorder)
9366
recorder.flush_trace(out_dir)
9467
end
9568

test/test_kernel_patches.rb

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# SPDX-License-Identifier: MIT
2+
3+
require 'minitest/autorun'
4+
require_relative '../codetracer/kernel_patches' # Adjust path as necessary
5+
6+
class MockTracer
7+
attr_reader :events, :name
8+
9+
def initialize(name = "tracer")
10+
@events = []
11+
@name = name
12+
end
13+
14+
def record_event(path, lineno, content)
15+
@events << { path: path, lineno: lineno, content: content }
16+
end
17+
18+
def clear_events
19+
@events = []
20+
end
21+
end
22+
23+
class TestKernelPatches < Minitest::Test
24+
def setup
25+
@tracer1 = MockTracer.new("tracer1")
26+
@tracer2 = MockTracer.new("tracer2")
27+
# Ensure a clean state before each test by attempting to clear any existing tracers
28+
# This is a bit of a hack, ideally KernelPatches would offer a reset or more direct access
29+
current_tracers = Codetracer::KernelPatches.class_variable_get(:@@tracers).dup
30+
current_tracers.each do |tracer|
31+
Codetracer::KernelPatches.uninstall(tracer)
32+
end
33+
end
34+
35+
def teardown
36+
# Ensure all tracers are uninstalled after each test
37+
current_tracers = Codetracer::KernelPatches.class_variable_get(:@@tracers).dup
38+
current_tracers.each do |tracer|
39+
Codetracer::KernelPatches.uninstall(tracer)
40+
end
41+
# Verify that original methods are restored if no tracers are left
42+
assert_empty Codetracer::KernelPatches.class_variable_get(:@@tracers), "Tracers should be empty after teardown"
43+
assert_empty Codetracer::KernelPatches.class_variable_get(:@@original_methods), "Original methods should be cleared if no tracers are left"
44+
end
45+
46+
def test_patching_and_basic_event_recording
47+
Codetracer::KernelPatches.install(@tracer1)
48+
49+
expected_line_p = __LINE__; p 'hello'
50+
expected_line_puts = __LINE__; puts 'world'
51+
expected_line_print = __LINE__; print 'test'
52+
53+
assert_equal 3, @tracer1.events.size
54+
55+
event_p = @tracer1.events[0]
56+
assert_equal __FILE__, event_p[:path]
57+
assert_equal expected_line_p, event_p[:lineno]
58+
assert_equal "\"hello\"", event_p[:content] # p uses inspect
59+
60+
event_puts = @tracer1.events[1]
61+
assert_equal __FILE__, event_puts[:path]
62+
assert_equal expected_line_puts, event_puts[:lineno]
63+
assert_equal "world", event_puts[:content]
64+
65+
event_print = @tracer1.events[2]
66+
assert_equal __FILE__, event_print[:path]
67+
assert_equal expected_line_print, event_print[:lineno]
68+
assert_equal "test", event_print[:content]
69+
70+
Codetracer::KernelPatches.uninstall(@tracer1)
71+
end
72+
73+
def test_multiple_tracers
74+
Codetracer::KernelPatches.install(@tracer1)
75+
Codetracer::KernelPatches.install(@tracer2)
76+
77+
expected_line_multi = __LINE__; p 'multitest'
78+
79+
assert_equal 1, @tracer1.events.size
80+
assert_equal 1, @tracer2.events.size
81+
82+
event1_multi = @tracer1.events.first
83+
assert_equal __FILE__, event1_multi[:path]
84+
assert_equal expected_line_multi, event1_multi[:lineno]
85+
assert_equal "\"multitest\"", event1_multi[:content]
86+
87+
event2_multi = @tracer2.events.first
88+
assert_equal __FILE__, event2_multi[:path]
89+
assert_equal expected_line_multi, event2_multi[:lineno]
90+
assert_equal "\"multitest\"", event2_multi[:content]
91+
92+
Codetracer::KernelPatches.uninstall(@tracer1)
93+
@tracer1.clear_events
94+
@tracer2.clear_events
95+
96+
expected_line_one_left = __LINE__; p 'one left'
97+
98+
assert_empty @tracer1.events, "Tracer1 should have no events after being uninstalled"
99+
assert_equal 1, @tracer2.events.size
100+
101+
event2_one_left = @tracer2.events.first
102+
assert_equal __FILE__, event2_one_left[:path]
103+
assert_equal expected_line_one_left, event2_one_left[:lineno]
104+
assert_equal "\"one left\"", event2_one_left[:content]
105+
106+
Codetracer::KernelPatches.uninstall(@tracer2)
107+
end
108+
109+
def test_restoration_of_original_methods
110+
Codetracer::KernelPatches.install(@tracer1)
111+
Codetracer::KernelPatches.uninstall(@tracer1)
112+
113+
# To truly test restoration, we'd capture stdout. Here, we focus on the tracer not being called.
114+
# If KernelPatches is working, uninstalling the last tracer should remove the patches.
115+
p 'original restored' # This line's output will go to actual stdout
116+
117+
assert_empty @tracer1.events, "Tracer should not record events after being uninstalled and patches removed"
118+
end
119+
120+
def test_correct_event_arguments
121+
Codetracer::KernelPatches.install(@tracer1)
122+
123+
arg_obj = { key: "value", number: 123 }
124+
125+
expected_line_p_detailed = __LINE__; p "detailed_p", arg_obj
126+
expected_line_puts_detailed = __LINE__; puts "detailed_puts", arg_obj.to_s
127+
expected_line_print_detailed = __LINE__; print "detailed_print", arg_obj.to_s
128+
129+
assert_equal 3, @tracer1.events.size
130+
131+
event_p = @tracer1.events[0]
132+
assert_equal __FILE__, event_p[:path], "Path for p mismatch"
133+
assert_equal expected_line_p_detailed, event_p[:lineno], "Line number for p mismatch"
134+
# p calls inspect on each argument and joins with newline if multiple, but here it's one string then obj
135+
assert_equal "\"detailed_p\"\n{:key=>\"value\", :number=>123}", event_p[:content], "Content for p mismatch"
136+
137+
138+
event_puts = @tracer1.events[1]
139+
assert_equal __FILE__, event_puts[:path], "Path for puts mismatch"
140+
assert_equal expected_line_puts_detailed, event_puts[:lineno], "Line number for puts mismatch"
141+
# puts calls to_s on each argument and prints each on a new line
142+
assert_equal "detailed_puts\n{:key=>\"value\", :number=>123}", event_puts[:content], "Content for puts mismatch"
143+
144+
145+
event_print = @tracer1.events[2]
146+
assert_equal __FILE__, event_print[:path], "Path for print mismatch"
147+
assert_equal expected_line_print_detailed, event_print[:lineno], "Line number for print mismatch"
148+
# print calls to_s on each argument and prints them sequentially
149+
assert_equal "detailed_print{:key=>\"value\", :number=>123}", event_print[:content], "Content for print mismatch"
150+
151+
Codetracer::KernelPatches.uninstall(@tracer1)
152+
end
153+
end

0 commit comments

Comments
 (0)