Skip to content

Instantly share code, notes, and snippets.

@jbodah
Created July 10, 2015 16:53
Show Gist options
  • Save jbodah/e1a9bbe1ab321b479b41 to your computer and use it in GitHub Desktop.
Save jbodah/e1a9bbe1ab321b479b41 to your computer and use it in GitHub Desktop.
if ENV['FG_STATS']
at_exit { require 'rubygems'; require 'pry'; binding.pry }
end
# Normal test helper code w/ Minitest
if ENV['FG_STATS']
class TestInefficiencyAnalyzer
def initialize(adapter)
case adapter
when :minitest
@adapter = MinitestAdapter.new
else
raise 'Unrecognized adapter!'
end
end
def start
@adapter.start
end
def improvements
@adapter.improvements
end
def report
puts
@adapter.improvements.each do |i|
# TODO remove redundancy
puts "In #{i[:test]}:"
i[:improvements].each do |im|
puts "\tAt #{im[:line]}: #{im[:from]} => #{im[:to]}"
end
puts
end
nil
end
class MinitestAdapter
MinitestSpy = Module.new
MinitestSpy.extend(Spy::API)
attr_accessor :improvements
def initialize
@improvements = []
end
def start
adapter = self
MinitestSpy.on(Minitest, :run_one_method)
.wrap do |r, test, method_name, *args, &block|
improvement = ImprovementFinder.find_improvements(adapter, r.name, test, block)
adapter.improvements << improvement if improvement
end
end
# Returns boolean whether test passed
def test_passes?(block)
result = block.call
result.passed?
end
end
class ImprovementRun
RunSpy = Module.new
RunSpy.extend(Spy::API)
attr_reader :inefficient, :efficient, :nth_call
attr_accessor :in_call, :caller
def initialize(adapter, test_block, inefficient, efficient, nth_call)
@adapter = adapter
@test_block = test_block
@inefficient = inefficient
@efficient = efficient
@nth_call = nth_call
@in_call = false
end
def run
manage_call_recursion
stub
run_test
ensure
clean_up
end
private
def manage_call_recursion
run = self
# Deal with #build calling create or #create calling build as well
# as recursion. Only work with top-level calls
[:create, :build].each do |sym|
RunSpy.on(FactoryGirl, sym).wrap do |*args, &block|
if run.in_call == false
run.in_call = true
block.call
run.in_call = false
end
end
end
end
def stub
run = self
call_count = 0
FactoryGirl.define_singleton_method inefficient.name, -> (*args) {
# recursionnnnn
if run.in_call
return inefficient.call(*args)
end
call_count += 1
if call_count == run.nth_call
# TODO: fix caller when recursive or composite call
run.caller = caller[0]
res = run.efficient.call(*args)
else
res = run.inefficient.call(*args)
end
}
end
def run_test
if @adapter.test_passes?(@test_block)
{
line: self.caller[/\d+/],
from: inefficient.name,
to: efficient.name,
#source: self.caller.sub(Rails.root.to_s, ''),
#index: nth_call
}
end
end
def clean_up
FactoryGirl.define_singleton_method inefficient.name, inefficient
RunSpy.restore(:all)
end
end
class ImprovementFinder
FgSpy = Module.new
FgSpy.extend(Spy::API)
def self.find_improvements(adapter, test, method_name, block)
new(adapter, test, method_name, block).find_improvements
end
# Find improvements in the given test, method, block
def initialize(adapter, test, method_name, block)
@adapter = adapter
@test = test
@method_name = method_name
@block = block
@improvements = []
# Save original method bindings
@fg_create = FactoryGirl.method(:create)
@fg_build = FactoryGirl.method(:build)
@fg_build_stubbed = FactoryGirl.method(:build_stubbed)
end
def find_improvements
dry_run
find_improvements_create
find_improvements_build
report
end
private
# Perform a dry run of the test to figure out how many times
# we call each inefficient method
def dry_run
in_spy = false # Don't count recursive/nested calls
create_spy = FgSpy.on(FactoryGirl, :create)
build_spy = FgSpy.on(FactoryGirl, :build)
[create_spy, build_spy].each do |s|
s.when { !in_spy }
s.before { in_spy = true }
s.after { in_spy = false }
end
@block.call
@dry_calls_create = create_spy.call_count
@dry_calls_build = build_spy.call_count
FgSpy.restore(:all)
end
# Try substituting each call to the inefficient method with
# the more efficient one. See if the tests still pass with
# the substitution
def find_improvements_create
(1..@dry_calls_create).to_a.each do |idx|
has_passed = try_create_to_build_stubbed(idx)
# #build_stubbed is better than #build; use it if we can
try_create_to_build(idx) if !has_passed
end
end
def find_improvements_build
(1..@dry_calls_build).to_a.each do |idx|
try_build_to_build_stubbed(idx)
end
end
def try_create_to_build(idx)
find_improvement(@fg_create, @fg_build, idx)
end
def try_create_to_build_stubbed(idx)
find_improvement(@fg_create, @fg_build_stubbed, idx)
end
def try_build_to_build_stubbed(idx)
find_improvement(@fg_build, @fg_build_stubbed, idx)
end
def report
if @improvements.any?
{
test: @test,
method_name: @method_name,
improvements: @improvements
}
end
end
# Stub out the nth call of the inefficient method with the
# efficient one. See if the tests still pass
def find_improvement(inefficient, efficient, nth_call)
improvement = ImprovementRun.new(@adapter, @block, inefficient, efficient, nth_call).run
@improvements << improvement if improvement
!!improvement
end
end
end
analyzer = TestInefficiencyAnalyzer.new(:minitest)
analyzer.start
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment