Created
July 10, 2015 16:53
-
-
Save jbodah/e1a9bbe1ab321b479b41 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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