test-prof 0.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/assets/flamegraph.demo.html +173 -0
- data/assets/flamegraph.template.html +196 -0
- data/assets/src/d3-tip.js +352 -0
- data/assets/src/d3-tip.min.js +1 -0
- data/assets/src/d3.flameGraph.css +92 -0
- data/assets/src/d3.flameGraph.js +459 -0
- data/assets/src/d3.flameGraph.min.css +1 -0
- data/assets/src/d3.flameGraph.min.js +1 -0
- data/assets/src/d3.v4.min.js +8 -0
- data/guides/any_fixture.md +60 -0
- data/guides/before_all.md +98 -0
- data/guides/event_prof.md +97 -0
- data/guides/factory_doctor.md +64 -0
- data/guides/factory_prof.md +85 -0
- data/guides/rspec_stamp.md +53 -0
- data/guides/rubocop.md +48 -0
- data/guides/ruby_prof.md +61 -0
- data/guides/stack_prof.md +43 -0
- data/lib/test-prof.rb +3 -0
- data/lib/test_prof/any_fixture.rb +67 -0
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +51 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +48 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +38 -0
- data/lib/test_prof/event_prof/custom_events.rb +5 -0
- data/lib/test_prof/event_prof/instrumentations/active_support.rb +16 -0
- data/lib/test_prof/event_prof/minitest.rb +3 -0
- data/lib/test_prof/event_prof/rspec.rb +94 -0
- data/lib/test_prof/event_prof.rb +177 -0
- data/lib/test_prof/ext/float_duration.rb +14 -0
- data/lib/test_prof/factory_doctor/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_doctor/minitest.rb +3 -0
- data/lib/test_prof/factory_doctor/rspec.rb +96 -0
- data/lib/test_prof/factory_doctor.rb +133 -0
- data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
- data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
- data/lib/test_prof/factory_prof.rb +140 -0
- data/lib/test_prof/logging.rb +25 -0
- data/lib/test_prof/recipes/rspec/any_fixture.rb +21 -0
- data/lib/test_prof/recipes/rspec/before_all.rb +23 -0
- data/lib/test_prof/rspec_stamp/parser.rb +103 -0
- data/lib/test_prof/rspec_stamp/rspec.rb +91 -0
- data/lib/test_prof/rspec_stamp.rb +135 -0
- data/lib/test_prof/rubocop.rb +3 -0
- data/lib/test_prof/ruby_prof/rspec.rb +13 -0
- data/lib/test_prof/ruby_prof.rb +194 -0
- data/lib/test_prof/stack_prof/rspec.rb +13 -0
- data/lib/test_prof/stack_prof.rb +120 -0
- data/lib/test_prof/version.rb +5 -0
- data/lib/test_prof.rb +108 -0
- metadata +227 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/factory_doctor/factory_girl_patch"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
# FactoryDoctor is a tool that helps you identify
|
7
|
+
# tests that perform unnecessary database queries.
|
8
|
+
module FactoryDoctor
|
9
|
+
class Result # :nodoc:
|
10
|
+
attr_reader :count, :time, :queries_count
|
11
|
+
|
12
|
+
def initialize(count, time, queries_count)
|
13
|
+
@count = count
|
14
|
+
@time = time
|
15
|
+
@queries_count = queries_count
|
16
|
+
end
|
17
|
+
|
18
|
+
def bad?
|
19
|
+
count.positive? && queries_count.zero?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
IGNORED_QUERIES_PATTERN = %r{(
|
24
|
+
pg_table|
|
25
|
+
pg_attribute|
|
26
|
+
pg_namespace|
|
27
|
+
show\stables|
|
28
|
+
pragma|
|
29
|
+
sqlite_master/rollback|
|
30
|
+
\ATRUNCATE TABLE|
|
31
|
+
\AALTER TABLE|
|
32
|
+
\ABEGIN|
|
33
|
+
\ACOMMIT|
|
34
|
+
\AROLLBACK|
|
35
|
+
\ARELEASE|
|
36
|
+
\ASAVEPOINT
|
37
|
+
)}xi
|
38
|
+
|
39
|
+
class << self
|
40
|
+
include TestProf::Logging
|
41
|
+
|
42
|
+
attr_reader :event
|
43
|
+
attr_reader :count, :time, :queries_count
|
44
|
+
|
45
|
+
# Patch factory lib, init counters
|
46
|
+
def init(event = 'sql.active_record')
|
47
|
+
@event = event
|
48
|
+
reset!
|
49
|
+
|
50
|
+
log :info, "FactoryDoctor enabled"
|
51
|
+
|
52
|
+
# Monkey-patch FactoryGirl
|
53
|
+
::FactoryGirl::FactoryRunner.prepend(FactoryGirlPatch) if
|
54
|
+
defined?(::FactoryGirl)
|
55
|
+
|
56
|
+
subscribe!
|
57
|
+
end
|
58
|
+
|
59
|
+
def start
|
60
|
+
reset!
|
61
|
+
@running = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop
|
65
|
+
@running = false
|
66
|
+
end
|
67
|
+
|
68
|
+
def result
|
69
|
+
Result.new(count, time, queries_count)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Do not analyze code within the block
|
73
|
+
def ignore
|
74
|
+
@ignored = true
|
75
|
+
res = yield
|
76
|
+
ensure
|
77
|
+
@ignored = false
|
78
|
+
res
|
79
|
+
end
|
80
|
+
|
81
|
+
def within_factory(strategy)
|
82
|
+
return yield if ignore? || !running? || (strategy != :create)
|
83
|
+
|
84
|
+
begin
|
85
|
+
ts = Time.now if @depth.zero?
|
86
|
+
@depth += 1
|
87
|
+
@count += 1
|
88
|
+
yield
|
89
|
+
ensure
|
90
|
+
@depth -= 1
|
91
|
+
|
92
|
+
@time += (Time.now - ts) if @depth.zero?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def reset!
|
99
|
+
@depth = 0
|
100
|
+
@time = 0.0
|
101
|
+
@count = 0
|
102
|
+
@queries_count = 0
|
103
|
+
end
|
104
|
+
|
105
|
+
def subscribe!
|
106
|
+
::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
|
107
|
+
next if ignore? || !running? || within_factory?
|
108
|
+
next if query[:sql] =~ IGNORED_QUERIES_PATTERN
|
109
|
+
@queries_count += 1
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def within_factory?
|
114
|
+
@depth.positive?
|
115
|
+
end
|
116
|
+
|
117
|
+
def ignore?
|
118
|
+
@ignored == true
|
119
|
+
end
|
120
|
+
|
121
|
+
def running?
|
122
|
+
@running == true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
require "test_prof/factory_doctor/rspec" if defined?(RSpec)
|
129
|
+
require "test_prof/factory_doctor/minitest" if defined?(Minitest::Reporters)
|
130
|
+
|
131
|
+
TestProf.activate('FDOC') do
|
132
|
+
TestProf::FactoryDoctor.init
|
133
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module TestProf::FactoryProf
|
6
|
+
module Printers
|
7
|
+
module Flamegraph # :nodoc: all
|
8
|
+
class << self
|
9
|
+
include TestProf::Logging
|
10
|
+
|
11
|
+
def dump(result)
|
12
|
+
report_data = {
|
13
|
+
total_stacks: result.stacks.size,
|
14
|
+
total: result.total
|
15
|
+
}
|
16
|
+
|
17
|
+
report_data[:roots] = convert_stacks(result)
|
18
|
+
|
19
|
+
path = generate_html(report_data)
|
20
|
+
|
21
|
+
log :info, "FactoryFlame report generated: #{path}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def convert_stacks(result)
|
25
|
+
res = []
|
26
|
+
|
27
|
+
paths = {}
|
28
|
+
|
29
|
+
result.stacks.each do |stack|
|
30
|
+
parent = nil
|
31
|
+
path = ""
|
32
|
+
|
33
|
+
stack.each do |sample|
|
34
|
+
path = "#{path}/#{sample}"
|
35
|
+
|
36
|
+
if paths[path]
|
37
|
+
node = paths[path]
|
38
|
+
node[:value] += 1
|
39
|
+
else
|
40
|
+
node = { name: sample, value: 1, total: result.raw_stats.fetch(sample)[:total] }
|
41
|
+
paths[path] = node
|
42
|
+
|
43
|
+
if parent.nil?
|
44
|
+
res << node
|
45
|
+
else
|
46
|
+
parent[:children] ||= []
|
47
|
+
parent[:children] << node
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
parent = node
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
res
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def generate_html(data)
|
61
|
+
template = File.read(TestProf.asset_path("flamegraph.template.html"))
|
62
|
+
template.sub! '/**REPORT-DATA**/', data.to_json
|
63
|
+
|
64
|
+
outpath = TestProf.artefact_path("factory-flame.html")
|
65
|
+
File.write(outpath, template)
|
66
|
+
outpath
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::FactoryProf
|
4
|
+
module Printers
|
5
|
+
module Simple # :nodoc: all
|
6
|
+
class << self
|
7
|
+
include TestProf::Logging
|
8
|
+
|
9
|
+
def dump(result)
|
10
|
+
msgs = []
|
11
|
+
|
12
|
+
msgs <<
|
13
|
+
<<~MSG
|
14
|
+
Factories usage
|
15
|
+
|
16
|
+
total top-level name
|
17
|
+
MSG
|
18
|
+
|
19
|
+
result.stats.each do |stat|
|
20
|
+
msgs << format("%6d %11d %15s", stat[:total], stat[:top_level], stat[:name])
|
21
|
+
end
|
22
|
+
|
23
|
+
log :info, msgs.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/factory_prof/factory_girl_patch"
|
4
|
+
require "test_prof/factory_prof/printers/simple"
|
5
|
+
require "test_prof/factory_prof/printers/flamegraph"
|
6
|
+
|
7
|
+
module TestProf
|
8
|
+
# FactoryProf collects "factory stacks" that can be used to build
|
9
|
+
# flamegraphs or detect most popular factories
|
10
|
+
module FactoryProf
|
11
|
+
# FactoryProf configuration
|
12
|
+
class Configuration
|
13
|
+
attr_accessor :mode
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@mode = ENV['FPROF'] == 'flamegraph' ? :flamegraph : :simple
|
17
|
+
end
|
18
|
+
|
19
|
+
# Whether we want to generate flamegraphs
|
20
|
+
def flamegraph?
|
21
|
+
@mode == :flamegraph
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Result # :nodoc:
|
26
|
+
attr_reader :stacks, :raw_stats
|
27
|
+
|
28
|
+
def initialize(stacks, raw_stats)
|
29
|
+
@stacks = stacks
|
30
|
+
@raw_stats = raw_stats
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns sorted stats
|
34
|
+
def stats
|
35
|
+
return @stats if instance_variable_defined?(:@stats)
|
36
|
+
|
37
|
+
@stats = @raw_stats.values
|
38
|
+
.sort_by { |el| -el[:total] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def total
|
42
|
+
return @total if instance_variable_defined?(:@total)
|
43
|
+
@total = @raw_stats.values.sum { |v| v[:total] }
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def sorted_stats(key)
|
49
|
+
@raw_stats.values
|
50
|
+
.map { |el| [el[:name], el[key]] }
|
51
|
+
.sort_by { |el| -el[1] }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
include TestProf::Logging
|
57
|
+
|
58
|
+
def config
|
59
|
+
@config ||= Configuration.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def configure
|
63
|
+
yield config
|
64
|
+
end
|
65
|
+
|
66
|
+
# Patch factory lib, init vars
|
67
|
+
def init
|
68
|
+
@running = false
|
69
|
+
|
70
|
+
log :info, "FactoryProf enabled (#{config.mode} mode)"
|
71
|
+
|
72
|
+
# Monkey-patch FactoryGirl
|
73
|
+
::FactoryGirl::FactoryRunner.prepend(FactoryGirlPatch) if
|
74
|
+
defined?(::FactoryGirl)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Inits FactoryProf and setups at exit hook,
|
78
|
+
# then runs
|
79
|
+
def run
|
80
|
+
init
|
81
|
+
|
82
|
+
printer = config.flamegraph? ? Printers::Flamegraph : Printers::Simple
|
83
|
+
|
84
|
+
at_exit { printer.dump(result) }
|
85
|
+
|
86
|
+
start
|
87
|
+
end
|
88
|
+
|
89
|
+
def start
|
90
|
+
reset!
|
91
|
+
@running = true
|
92
|
+
end
|
93
|
+
|
94
|
+
def stop
|
95
|
+
@running = false
|
96
|
+
end
|
97
|
+
|
98
|
+
def result
|
99
|
+
Result.new(@stacks, @stats)
|
100
|
+
end
|
101
|
+
|
102
|
+
def track(strategy, factory)
|
103
|
+
return yield if !running? || (strategy != :create)
|
104
|
+
begin
|
105
|
+
@depth += 1
|
106
|
+
@current_stack << factory if config.flamegraph?
|
107
|
+
@stats[factory][:total] += 1
|
108
|
+
@stats[factory][:top_level] += 1 if @depth == 1
|
109
|
+
yield
|
110
|
+
ensure
|
111
|
+
@depth -= 1
|
112
|
+
flush_stack if @depth.zero?
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def reset!
|
119
|
+
@stacks = [] if config.flamegraph?
|
120
|
+
@depth = 0
|
121
|
+
@stats = Hash.new { |h, k| h[k] = { name: k, total: 0, top_level: 0 } }
|
122
|
+
flush_stack
|
123
|
+
end
|
124
|
+
|
125
|
+
def flush_stack
|
126
|
+
return unless config.flamegraph?
|
127
|
+
@stacks << @current_stack unless @current_stack.nil? || @current_stack.empty?
|
128
|
+
@current_stack = []
|
129
|
+
end
|
130
|
+
|
131
|
+
def running?
|
132
|
+
@running == true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
TestProf.activate('FPROF') do
|
139
|
+
TestProf::FactoryProf.run
|
140
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
# Helper for output printing
|
5
|
+
module Logging
|
6
|
+
COLORS = {
|
7
|
+
info: "\e[34m", # blue
|
8
|
+
error: "\e[31m", # red
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
def log(level, msg)
|
12
|
+
TestProf.config.output.puts(build_log_msg(level, msg))
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_log_msg(level, msg)
|
16
|
+
colorize(level, "[TEST PROF #{level.to_s.upcase}] #{msg}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def colorize(level, msg)
|
20
|
+
return msg unless TestProf.config.color?
|
21
|
+
|
22
|
+
"#{COLORS[level]}#{msg}\e[0m"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/any_fixture"
|
4
|
+
|
5
|
+
RSpec.shared_context "any_fixture:clean", with_clean_fixture: true do
|
6
|
+
before do
|
7
|
+
raise("Cannot use clean context without a transaction!") unless
|
8
|
+
open_transaction?
|
9
|
+
|
10
|
+
TestProf::AnyFixture.clean
|
11
|
+
end
|
12
|
+
|
13
|
+
def open_transaction?
|
14
|
+
pool = ActiveRecord::Base.connection_pool
|
15
|
+
pool.active_connection? && pool.connection.open_transactions.positive?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.after(:suite) { TestProf::AnyFixture.reset }
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
# Helper to wrap the whole example group into a transaction
|
5
|
+
module BeforeAll
|
6
|
+
def before_all(&block)
|
7
|
+
raise ArgumentError, "Block is required!" unless block_given?
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:all) do
|
15
|
+
ActiveRecord::Base.connection.rollback_transaction
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.extend TestProf::BeforeAll
|
23
|
+
end
|