Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
alias t := test

test:
ruby -Itest test/test_tracer.rb
ruby -Itest test/test_kernel_patches.rb test/test_tracer.rb

bench pattern="*" write_report="console":
ruby test/benchmarks/run_benchmarks.rb '{{pattern}}' --write-report={{write_report}}
Expand Down
61 changes: 61 additions & 0 deletions codetracer/kernel_patches.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: MIT

module Codetracer
module KernelPatches
@@tracers = []
@@original_methods = {}

def self.install(tracer)
@@tracers << tracer

if @@original_methods.empty?
@@original_methods[:p] = Kernel.instance_method(:p)
@@original_methods[:puts] = Kernel.instance_method(:puts)
@@original_methods[:print] = Kernel.instance_method(:print)

Kernel.module_eval do
alias_method :old_p, :p unless method_defined?(:old_p)
alias_method :old_puts, :puts unless method_defined?(:old_puts)
alias_method :old_print, :print unless method_defined?(:old_print)

define_method(:p) do |*args|
loc = caller_locations(1,1).first
@@tracers.each do |t|
t.record_event(loc.path, loc.lineno, args.map(&:inspect).join("\n"))
end
@@original_methods[:p].bind(self).call(*args)
end

define_method(:puts) do |*args|
loc = caller_locations(1,1).first
@@tracers.each do |t|
t.record_event(loc.path, loc.lineno, args.join("\n"))
end
@@original_methods[:puts].bind(self).call(*args)
end

define_method(:print) do |*args|
loc = caller_locations(1,1).first
@@tracers.each do |t|
t.record_event(loc.path, loc.lineno, args.join)
end
@@original_methods[:print].bind(self).call(*args)
end
end
end
end

def self.uninstall(tracer)
@@tracers.delete(tracer)

if @@tracers.empty? && !@@original_methods.empty?
Kernel.module_eval do
define_method(:p, @@original_methods[:p])
define_method(:puts, @@original_methods[:puts])
define_method(:print, @@original_methods[:print])
end
@@original_methods.clear
end
end
end
end
46 changes: 5 additions & 41 deletions gems/codetracer-pure-ruby-recorder/lib/trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'json'
require 'optparse'
require_relative 'recorder'
require_relative '../../../codetracer/kernel_patches'


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

# instrumentation helpers for recording IO calls
module CodetracerKernelPatches
def self.install(tracer)
Kernel.module_eval do
unless method_defined?(:old_p)
alias :old_p :p
alias :old_puts :puts
alias :old_print :print
end

define_method(:p) do |*args|
if tracer.tracing
tracer.deactivate
tracer.record_event(caller, args.join("\n"))
tracer.activate
end
old_p(*args)
end

define_method(:puts) do |*args|
if tracer.tracing
tracer.deactivate
tracer.record_event(caller, args.join("\n"))
tracer.activate
end
old_puts(*args)
end

define_method(:print) do |*args|
if tracer.tracing
tracer.deactivate
tracer.record_event(caller, args.join("\n"))
tracer.activate
end
old_print(*args)
end
end
end
end

# class IO
# alias :old_write :write

Expand Down Expand Up @@ -257,7 +218,7 @@ def load_variables(binding)

if __FILE__ == $PROGRAM_NAME
$tracer = Tracer.new($codetracer_record, debug: ENV['CODETRACER_RUBY_RECORDER_DEBUG'] == '1')
CodetracerKernelPatches.install($tracer)
::Codetracer::KernelPatches.install($tracer)

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

$tracer.activate
at_exit do
::Codetracer::KernelPatches.uninstall($tracer)
end
begin
Kernel.load(program)
rescue Exception => e
Expand Down
33 changes: 3 additions & 30 deletions gems/codetracer-ruby-recorder/lib/native_trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'optparse'
require 'fileutils'
require 'rbconfig'
require_relative '../../../codetracer/kernel_patches'

options = {}
parser = OptionParser.new do |opts|
Expand Down Expand Up @@ -50,36 +51,7 @@
require target_path
recorder = RubyRecorder.new
$recorder = recorder

module Kernel
alias :old_p :p
alias :old_puts :puts
alias :old_print :print

def p(*args)
if $recorder
loc = caller_locations(1,1).first
$recorder.record_event(loc.path, loc.lineno, args.join("\n"))
end
old_p(*args)
end

def puts(*args)
if $recorder
loc = caller_locations(1,1).first
$recorder.record_event(loc.path, loc.lineno, args.join("\n"))
end
old_puts(*args)
end

def print(*args)
if $recorder
loc = caller_locations(1,1).first
$recorder.record_event(loc.path, loc.lineno, args.join("\n"))
end
old_print(*args)
end
end
::Codetracer::KernelPatches.install(recorder)

rescue Exception => e
warn "native tracer unavailable: #{e}"
Expand All @@ -90,6 +62,7 @@ def print(*args)
load program
if recorder
recorder.disable_tracing
::Codetracer::KernelPatches.uninstall(recorder)
recorder.flush_trace(out_dir)
end

153 changes: 153 additions & 0 deletions test/test_kernel_patches.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# SPDX-License-Identifier: MIT

require 'minitest/autorun'
require_relative '../codetracer/kernel_patches' # Adjust path as necessary

class MockTracer
attr_reader :events, :name

def initialize(name = "tracer")
@events = []
@name = name
end

def record_event(path, lineno, content)
@events << { path: path, lineno: lineno, content: content }
end

def clear_events
@events = []
end
end

class TestKernelPatches < Minitest::Test
def setup
@tracer1 = MockTracer.new("tracer1")
@tracer2 = MockTracer.new("tracer2")
# Ensure a clean state before each test by attempting to clear any existing tracers
# This is a bit of a hack, ideally KernelPatches would offer a reset or more direct access
current_tracers = Codetracer::KernelPatches.class_variable_get(:@@tracers).dup
current_tracers.each do |tracer|
Codetracer::KernelPatches.uninstall(tracer)
end
end

def teardown
# Ensure all tracers are uninstalled after each test
current_tracers = Codetracer::KernelPatches.class_variable_get(:@@tracers).dup
current_tracers.each do |tracer|
Codetracer::KernelPatches.uninstall(tracer)
end
# Verify that original methods are restored if no tracers are left
assert_empty Codetracer::KernelPatches.class_variable_get(:@@tracers), "Tracers should be empty after teardown"
assert_empty Codetracer::KernelPatches.class_variable_get(:@@original_methods), "Original methods should be cleared if no tracers are left"
end

def test_patching_and_basic_event_recording
Codetracer::KernelPatches.install(@tracer1)

expected_line_p = __LINE__; p 'hello'
expected_line_puts = __LINE__; puts 'world'
expected_line_print = __LINE__; print 'test'

assert_equal 3, @tracer1.events.size

event_p = @tracer1.events[0]
assert_equal __FILE__, event_p[:path]
assert_equal expected_line_p, event_p[:lineno]
assert_equal "\"hello\"", event_p[:content] # p uses inspect

event_puts = @tracer1.events[1]
assert_equal __FILE__, event_puts[:path]
assert_equal expected_line_puts, event_puts[:lineno]
assert_equal "world", event_puts[:content]

event_print = @tracer1.events[2]
assert_equal __FILE__, event_print[:path]
assert_equal expected_line_print, event_print[:lineno]
assert_equal "test", event_print[:content]

Codetracer::KernelPatches.uninstall(@tracer1)
end

def test_multiple_tracers
Codetracer::KernelPatches.install(@tracer1)
Codetracer::KernelPatches.install(@tracer2)

expected_line_multi = __LINE__; p 'multitest'

assert_equal 1, @tracer1.events.size
assert_equal 1, @tracer2.events.size

event1_multi = @tracer1.events.first
assert_equal __FILE__, event1_multi[:path]
assert_equal expected_line_multi, event1_multi[:lineno]
assert_equal "\"multitest\"", event1_multi[:content]

event2_multi = @tracer2.events.first
assert_equal __FILE__, event2_multi[:path]
assert_equal expected_line_multi, event2_multi[:lineno]
assert_equal "\"multitest\"", event2_multi[:content]

Codetracer::KernelPatches.uninstall(@tracer1)
@tracer1.clear_events
@tracer2.clear_events

expected_line_one_left = __LINE__; p 'one left'

assert_empty @tracer1.events, "Tracer1 should have no events after being uninstalled"
assert_equal 1, @tracer2.events.size

event2_one_left = @tracer2.events.first
assert_equal __FILE__, event2_one_left[:path]
assert_equal expected_line_one_left, event2_one_left[:lineno]
assert_equal "\"one left\"", event2_one_left[:content]

Codetracer::KernelPatches.uninstall(@tracer2)
end

def test_restoration_of_original_methods
Codetracer::KernelPatches.install(@tracer1)
Codetracer::KernelPatches.uninstall(@tracer1)

# To truly test restoration, we'd capture stdout. Here, we focus on the tracer not being called.
# If KernelPatches is working, uninstalling the last tracer should remove the patches.
p 'original restored' # This line's output will go to actual stdout

assert_empty @tracer1.events, "Tracer should not record events after being uninstalled and patches removed"
end

def test_correct_event_arguments
Codetracer::KernelPatches.install(@tracer1)

arg_obj = { key: "value", number: 123 }

expected_line_p_detailed = __LINE__; p "detailed_p", arg_obj
expected_line_puts_detailed = __LINE__; puts "detailed_puts", arg_obj.to_s
expected_line_print_detailed = __LINE__; print "detailed_print", arg_obj.to_s

assert_equal 3, @tracer1.events.size

event_p = @tracer1.events[0]
assert_equal __FILE__, event_p[:path], "Path for p mismatch"
assert_equal expected_line_p_detailed, event_p[:lineno], "Line number for p mismatch"
# p calls inspect on each argument and joins with newline if multiple, but here it's one string then obj
assert_equal "\"detailed_p\"\n{:key=>\"value\", :number=>123}", event_p[:content], "Content for p mismatch"


event_puts = @tracer1.events[1]
assert_equal __FILE__, event_puts[:path], "Path for puts mismatch"
assert_equal expected_line_puts_detailed, event_puts[:lineno], "Line number for puts mismatch"
# puts calls to_s on each argument and prints each on a new line
assert_equal "detailed_puts\n{:key=>\"value\", :number=>123}", event_puts[:content], "Content for puts mismatch"


event_print = @tracer1.events[2]
assert_equal __FILE__, event_print[:path], "Path for print mismatch"
assert_equal expected_line_print_detailed, event_print[:lineno], "Line number for print mismatch"
# print calls to_s on each argument and prints them sequentially
assert_equal "detailed_print{:key=>\"value\", :number=>123}", event_print[:content], "Content for print mismatch"

Codetracer::KernelPatches.uninstall(@tracer1)
end
end
Loading