Skip to content

Instantly share code, notes, and snippets.

@palkan
Created March 27, 2017 15:42
Show Gist options
  • Save palkan/4088ab57191f6b537ef3e4d75939a190 to your computer and use it in GitHub Desktop.
Save palkan/4088ab57191f6b537ef3e4d75939a190 to your computer and use it in GitHub Desktop.
FactoryDoc: detect useless data generation in tests
module FactoryGirl
module Doctor
module FloatDuration
refine Float do
def duration
t = self
format("%02d:%02d.%03d", t / 60, t % 60, t.modulo(1) * 1000)
end
end
end
using FloatDuration
module FactoryExt
def run(strategy = @strategy)
Doctor.within_factory(strategy) { super }
end
end
class << self
attr_reader :count, :time
def init
@depth = 0
reset
FactoryGirl::FactoryRunner.prepend FactoryExt
end
def within_factory(strategy)
return yield if ignore?
ts = Time.now if @depth.zero?
@depth += 1
@count += 1 if strategy == :create
yield
ensure
@depth -= 1
if @depth.zero?
delta = (Time.now - ts)
@time += delta
end
end
def ignore
@ignored = true
res = yield
@ignored = false
res
end
def reset
@count = 0
@time = 0.0
end
def within_factory?
@depth.positive?
end
def ignore?
@ignored == true
end
end
class Profiler
IGNORED_QUERIES_PATTERN = %r{(
pg_table|
pg_attribute|
pg_namespace|
show\stables|
pragma|
sqlite_master/rollback|
\ATRUNCATE TABLE|
\AALTER TABLE|
\ABEGIN|
\ACOMMIT|
\AROLLBACK|
\ARELEASE|
\ASAVEPOINT
)}xi
NOTIFICATIONS = [:example_started, :example_finished].freeze
def initialize
@queries = []
@example_groups = Hash.new { |h, k| h[k] = [] }
ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _start, _finish, _id, query|
next if Doctor.within_factory?
next if query[:sql] =~ IGNORED_QUERIES_PATTERN
@queries << query[:sql]
end
end
def example_started(_notification)
@queries.clear
Doctor.reset
end
def example_finished(notification)
return if notification.example.pending?
if Doctor.count.positive? && @queries.size.zero?
group = notification.example.example_group.parent_groups.last
notification.example.metadata.merge!(
factories: Doctor.count,
time: Doctor.time
)
@example_groups[group] << notification.example
end
end
def print
return if @example_groups.empty?
output.puts(
"\n\nFactoryDoctor found useless data generation "\
"in the following examples\n"
)
total_time = 0.0
@example_groups.each do |group, examples|
out = ["#{group.description} (#{group.metadata[:location]})"
examples.each do |ex|
total_time += ex.metadata[:time]
out << " #{ex.description} (#{ex.metadata[:location]}) – #{ex.metadata[:factories]} created objects, #{ex.metadata[:time].duration}"
end
output.puts out.join("\n")
end
output.puts "\nTotal wasted time: #{total_time.duration}\n"
end
private
def output
RSpec.configuration.output_stream
end
end
end
end
if ENV['FDOC']
FactoryGirl::Doctor.init
RSpec.configure do |config|
listener = FactoryGirl::Doctor::Profiler.new
config.reporter.register_listener(listener, *FactoryGirl::Doctor::Profiler::NOTIFICATIONS)
config.after(:suite) { listener.print }
end
end
@JeremiahChurch
Copy link

JeremiahChurch commented Jul 28, 2017

use FDOC=flamegraph rspec if you want a flamegraph generated

@anhtranEH
Copy link

@AntonErmolenko
Copy link

I've cloned the gist and I'd corrected some errors
https://gist.github.com/AntonErmolenko/d927eec32d1c1382c0ef559af30f602c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment