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,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::EventProf::CustomEvents
|
4
|
+
module SidekiqJobs # :nodoc: all
|
5
|
+
module ClientPatch
|
6
|
+
def raw_push(*)
|
7
|
+
return super unless Sidekiq::Testing.inline?
|
8
|
+
SidekiqJobs.track { super }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def setup!
|
14
|
+
Sidekiq::Client.prepend ClientPatch
|
15
|
+
end
|
16
|
+
|
17
|
+
def track
|
18
|
+
ActiveSupport::Notifications.instrument(
|
19
|
+
'sidekiq.jobs'
|
20
|
+
) { yield }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if TestProf.require(
|
27
|
+
'sidekiq/testing',
|
28
|
+
<<~MSG
|
29
|
+
Failed to load Sidekiq.
|
30
|
+
|
31
|
+
Make sure that "sidekiq" gem is in your Gemfile.
|
32
|
+
MSG
|
33
|
+
)
|
34
|
+
TestProf::EventProf::CustomEvents::SidekiqJobs.setup!
|
35
|
+
TestProf::EventProf.configure do |config|
|
36
|
+
config.rank_by = :count
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/event_prof/custom_events/factory_create" if ENV['EVENT_PROF'] == "factory.create"
|
4
|
+
require "test_prof/event_prof/custom_events/sidekiq_inline" if ENV['EVENT_PROF'] == "sidekiq.inline"
|
5
|
+
require "test_prof/event_prof/custom_events/sidekiq_jobs" if ENV['EVENT_PROF'] == "sidekiq.jobs"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::EventProf
|
4
|
+
module Instrumentations
|
5
|
+
# Wrapper over ActiveSupport::Notifications
|
6
|
+
module ActiveSupport
|
7
|
+
def self.subscribe(event)
|
8
|
+
raise ArgumentError, 'Block is required!' unless block_given?
|
9
|
+
|
10
|
+
::ActiveSupport::Notifications.subscribe(event) do |_event, start, finish, *_args|
|
11
|
+
yield (finish - start)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/ext/float_duration"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module EventProf
|
7
|
+
class RSpecListener # :nodoc:
|
8
|
+
include Logging
|
9
|
+
using FloatDuration
|
10
|
+
|
11
|
+
NOTIFICATIONS = %i[
|
12
|
+
example_group_started
|
13
|
+
example_group_finished
|
14
|
+
example_started
|
15
|
+
example_finished
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@profiler = EventProf.build
|
20
|
+
end
|
21
|
+
|
22
|
+
def example_group_started(notification)
|
23
|
+
return unless notification.group.top_level?
|
24
|
+
@profiler.group_started notification.group
|
25
|
+
end
|
26
|
+
|
27
|
+
def example_group_finished(notification)
|
28
|
+
return unless notification.group.top_level?
|
29
|
+
@profiler.group_finished notification.group
|
30
|
+
end
|
31
|
+
|
32
|
+
def example_started(notification)
|
33
|
+
@profiler.example_started notification.example
|
34
|
+
end
|
35
|
+
|
36
|
+
def example_finished(notification)
|
37
|
+
@profiler.example_finished notification.example
|
38
|
+
end
|
39
|
+
|
40
|
+
def print
|
41
|
+
result = @profiler.results
|
42
|
+
|
43
|
+
msgs = []
|
44
|
+
|
45
|
+
msgs <<
|
46
|
+
<<~MSG
|
47
|
+
EventProf results for #{@profiler.event}
|
48
|
+
|
49
|
+
Total time: #{@profiler.total_time.duration}
|
50
|
+
Total events: #{@profiler.total_count}
|
51
|
+
|
52
|
+
Top #{@profiler.top_count} slowest suites (by #{@profiler.rank_by}):
|
53
|
+
|
54
|
+
MSG
|
55
|
+
|
56
|
+
result[:groups].each do |group|
|
57
|
+
description = group[:id].top_level_description
|
58
|
+
location = group[:id].metadata[:location]
|
59
|
+
|
60
|
+
msgs <<
|
61
|
+
<<~GROUP
|
62
|
+
#{description} (#{location}) – #{group[:time].duration} (#{group[:count]} / #{group[:examples]})
|
63
|
+
GROUP
|
64
|
+
end
|
65
|
+
|
66
|
+
if result[:examples]
|
67
|
+
msgs << "\nTop #{@profiler.top_count} slowest tests (by #{@profiler.rank_by}):\n\n"
|
68
|
+
|
69
|
+
result[:examples].each do |example|
|
70
|
+
description = example[:id].description
|
71
|
+
location = example[:id].metadata[:location]
|
72
|
+
msgs <<
|
73
|
+
<<~GROUP
|
74
|
+
#{description} (#{location}) – #{example[:time].duration} (#{example[:count]})
|
75
|
+
GROUP
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
log :info, msgs.join
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Register EventProf listener
|
86
|
+
TestProf.activate('EVENT_PROF') do
|
87
|
+
RSpec.configure do |config|
|
88
|
+
listener = TestProf::EventProf::RSpecListener.new
|
89
|
+
|
90
|
+
config.reporter.register_listener(listener, *TestProf::EventProf::RSpecListener::NOTIFICATIONS)
|
91
|
+
|
92
|
+
config.after(:suite) { listener.print }
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/event_prof/instrumentations/active_support"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
# EventProf profiles your tests and suites against custom events,
|
7
|
+
# such as ActiveSupport::Notifacations.
|
8
|
+
#
|
9
|
+
# It works very similar to `rspec --profile` but can track arbitrary events.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# # Collect SQL queries stats for every suite and example
|
14
|
+
# EVENT_PROF='sql.active_record' rspec ...
|
15
|
+
#
|
16
|
+
# By default it collects information only about top-level groups (aka suites),
|
17
|
+
# but you can also profile individual examples. Just set the configuration option:
|
18
|
+
#
|
19
|
+
# TestProf::EventProf.configure do |config|
|
20
|
+
# config.per_example = true
|
21
|
+
# end
|
22
|
+
module EventProf
|
23
|
+
# EventProf configuration
|
24
|
+
class Configuration
|
25
|
+
# Map of supported instrumenters
|
26
|
+
INSTRUMENTERS = {
|
27
|
+
active_support: 'ActiveSupport'
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
attr_accessor :instrumenter, :top_count, :per_example,
|
31
|
+
:rank_by, :event
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@event = ENV['EVENT_PROF']
|
35
|
+
@instrumenter = :active_support
|
36
|
+
@top_count = (ENV['EVENT_PROF_TOP'] || 5).to_i
|
37
|
+
@per_example = false
|
38
|
+
@rank_by = (ENV['EVENT_PROF_RANK'] || :time).to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
def per_example?
|
42
|
+
per_example == true
|
43
|
+
end
|
44
|
+
|
45
|
+
def resolve_instrumenter
|
46
|
+
return instrumenter if instrumenter.is_a?(Module)
|
47
|
+
|
48
|
+
raise ArgumentError, "Unknown instrumenter: #{instrumenter}" unless
|
49
|
+
INSTRUMENTERS.key?(instrumenter)
|
50
|
+
|
51
|
+
Instrumentations.const_get(INSTRUMENTERS[instrumenter])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def config
|
57
|
+
@config ||= Configuration.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def configure
|
61
|
+
yield config
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns new configured instance of profiler
|
65
|
+
def build
|
66
|
+
Profiler.new(
|
67
|
+
event: config.event,
|
68
|
+
instrumenter: config.resolve_instrumenter
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Profiler # :nodoc:
|
74
|
+
include TestProf::Logging
|
75
|
+
|
76
|
+
attr_reader :event, :top_count, :rank_by, :total_count, :total_time
|
77
|
+
|
78
|
+
def initialize(event:, instrumenter:)
|
79
|
+
@event = event
|
80
|
+
|
81
|
+
log :info, "EventProf enabled (#{@event})"
|
82
|
+
|
83
|
+
instrumenter.subscribe(event) { |time| track(time) }
|
84
|
+
|
85
|
+
@groups = Hash.new { |h, k| h[k] = { id: k } }
|
86
|
+
@examples = Hash.new { |h, k| h[k] = { id: k } }
|
87
|
+
|
88
|
+
@total_count = 0
|
89
|
+
@total_time = 0.0
|
90
|
+
|
91
|
+
reset!
|
92
|
+
end
|
93
|
+
|
94
|
+
def track(time)
|
95
|
+
return if @current_group.nil?
|
96
|
+
@total_time += time
|
97
|
+
@total_count += 1
|
98
|
+
|
99
|
+
@time += time
|
100
|
+
@count += 1
|
101
|
+
|
102
|
+
@example_time += time if config.per_example?
|
103
|
+
@example_count += 1 if config.per_example?
|
104
|
+
end
|
105
|
+
|
106
|
+
def group_started(id)
|
107
|
+
reset!
|
108
|
+
@current_group = id
|
109
|
+
end
|
110
|
+
|
111
|
+
def group_finished(id)
|
112
|
+
@groups[id][:time] = @time
|
113
|
+
@groups[id][:count] = @count
|
114
|
+
@groups[id][:examples] = @total_examples
|
115
|
+
@current_group = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def example_started(_id)
|
119
|
+
reset_example!
|
120
|
+
end
|
121
|
+
|
122
|
+
def example_finished(id)
|
123
|
+
@total_examples += 1
|
124
|
+
return unless config.per_example?
|
125
|
+
|
126
|
+
@examples[id][:time] = @example_time
|
127
|
+
@examples[id][:count] = @example_count
|
128
|
+
end
|
129
|
+
|
130
|
+
def results
|
131
|
+
{
|
132
|
+
groups: fetch_top(@groups.values)
|
133
|
+
}.tap do |data|
|
134
|
+
next unless config.per_example?
|
135
|
+
|
136
|
+
data[:examples] = fetch_top(@examples.values)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def rank_by
|
141
|
+
EventProf.config.rank_by
|
142
|
+
end
|
143
|
+
|
144
|
+
def top_count
|
145
|
+
EventProf.config.top_count
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def fetch_top(arr)
|
151
|
+
arr.reject { |el| el[rank_by].zero? }
|
152
|
+
.sort_by { |el| -el[rank_by] }
|
153
|
+
.take(top_count)
|
154
|
+
end
|
155
|
+
|
156
|
+
def config
|
157
|
+
EventProf.config
|
158
|
+
end
|
159
|
+
|
160
|
+
def reset!
|
161
|
+
@time = 0.0
|
162
|
+
@count = 0
|
163
|
+
@total_examples = 0
|
164
|
+
reset_example!
|
165
|
+
end
|
166
|
+
|
167
|
+
def reset_example!
|
168
|
+
@example_count = 0
|
169
|
+
@example_time = 0.0
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
require "test_prof/event_prof/rspec" if defined?(RSpec)
|
176
|
+
require "test_prof/event_prof/minitest" if defined?(Minitest::Reporters)
|
177
|
+
require "test_prof/event_prof/custom_events"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add #duration method to floats
|
4
|
+
module TestProf
|
5
|
+
# Extend Float with #duration method
|
6
|
+
module FloatDuration
|
7
|
+
refine Float do
|
8
|
+
def duration
|
9
|
+
t = self
|
10
|
+
format("%02d:%02d.%03d", t / 60, t % 60, t.modulo(1) * 1000)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module FactoryDoctor
|
5
|
+
# Wrap #run method with FactoryDoctor tracking
|
6
|
+
module FactoryGirlPatch
|
7
|
+
def run(strategy = @strategy)
|
8
|
+
FactoryDoctor.within_factory(strategy) { super }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/ext/float_duration"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module FactoryDoctor
|
7
|
+
class RSpecListener # :nodoc:
|
8
|
+
include Logging
|
9
|
+
using FloatDuration
|
10
|
+
|
11
|
+
NOTIFICATIONS = %i[
|
12
|
+
example_started
|
13
|
+
example_finished
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@count = 0
|
18
|
+
@time = 0.0
|
19
|
+
@example_groups = Hash.new { |h, k| h[k] = [] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def example_started(_notification)
|
23
|
+
FactoryDoctor.start
|
24
|
+
end
|
25
|
+
|
26
|
+
def example_finished(notification)
|
27
|
+
FactoryDoctor.stop
|
28
|
+
return if notification.example.pending?
|
29
|
+
|
30
|
+
result = FactoryDoctor.result
|
31
|
+
|
32
|
+
return unless result.bad?
|
33
|
+
|
34
|
+
group = notification.example.example_group.parent_groups.last
|
35
|
+
notification.example.metadata.merge!(
|
36
|
+
factories: result.count,
|
37
|
+
time: result.time
|
38
|
+
)
|
39
|
+
@example_groups[group] << notification.example
|
40
|
+
@count += 1
|
41
|
+
@time += result.time
|
42
|
+
end
|
43
|
+
|
44
|
+
def print
|
45
|
+
return if @example_groups.empty?
|
46
|
+
|
47
|
+
msgs = []
|
48
|
+
|
49
|
+
msgs <<
|
50
|
+
<<~MSG
|
51
|
+
FactoryDoctor report
|
52
|
+
|
53
|
+
Total (potentially) bad examples: #{@count}
|
54
|
+
Total wasted time: #{@time.duration}
|
55
|
+
|
56
|
+
MSG
|
57
|
+
|
58
|
+
@example_groups.each do |group, examples|
|
59
|
+
msgs << "#{group.description} (#{group.metadata[:location]})\n"
|
60
|
+
examples.each do |ex|
|
61
|
+
msgs << " #{ex.description} (#{ex.metadata[:location]}) "\
|
62
|
+
"– #{pluralize_records(ex.metadata[:factories])} created, "\
|
63
|
+
"#{ex.metadata[:time].duration}\n"
|
64
|
+
end
|
65
|
+
msgs << "\n"
|
66
|
+
end
|
67
|
+
|
68
|
+
log :info, msgs.join
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def pluralize_records(count)
|
74
|
+
return "1 record" if count == 1
|
75
|
+
"#{count} records"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Register FactoryDoctor listener
|
82
|
+
TestProf.activate('FDOC') do
|
83
|
+
RSpec.configure do |config|
|
84
|
+
listener = TestProf::FactoryDoctor::RSpecListener.new
|
85
|
+
|
86
|
+
config.reporter.register_listener(
|
87
|
+
listener, *TestProf::FactoryDoctor::RSpecListener::NOTIFICATIONS
|
88
|
+
)
|
89
|
+
|
90
|
+
config.after(:suite) { listener.print }
|
91
|
+
end
|
92
|
+
|
93
|
+
RSpec.shared_context "factory_doctor:ignore", fd_ignore: true do
|
94
|
+
around(:each) { |ex| TestProf::FactoryDoctor.ignore(&ex) }
|
95
|
+
end
|
96
|
+
end
|