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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +103 -0
  5. data/assets/flamegraph.demo.html +173 -0
  6. data/assets/flamegraph.template.html +196 -0
  7. data/assets/src/d3-tip.js +352 -0
  8. data/assets/src/d3-tip.min.js +1 -0
  9. data/assets/src/d3.flameGraph.css +92 -0
  10. data/assets/src/d3.flameGraph.js +459 -0
  11. data/assets/src/d3.flameGraph.min.css +1 -0
  12. data/assets/src/d3.flameGraph.min.js +1 -0
  13. data/assets/src/d3.v4.min.js +8 -0
  14. data/guides/any_fixture.md +60 -0
  15. data/guides/before_all.md +98 -0
  16. data/guides/event_prof.md +97 -0
  17. data/guides/factory_doctor.md +64 -0
  18. data/guides/factory_prof.md +85 -0
  19. data/guides/rspec_stamp.md +53 -0
  20. data/guides/rubocop.md +48 -0
  21. data/guides/ruby_prof.md +61 -0
  22. data/guides/stack_prof.md +43 -0
  23. data/lib/test-prof.rb +3 -0
  24. data/lib/test_prof/any_fixture.rb +67 -0
  25. data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
  26. data/lib/test_prof/event_prof/custom_events/factory_create.rb +51 -0
  27. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +48 -0
  28. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +38 -0
  29. data/lib/test_prof/event_prof/custom_events.rb +5 -0
  30. data/lib/test_prof/event_prof/instrumentations/active_support.rb +16 -0
  31. data/lib/test_prof/event_prof/minitest.rb +3 -0
  32. data/lib/test_prof/event_prof/rspec.rb +94 -0
  33. data/lib/test_prof/event_prof.rb +177 -0
  34. data/lib/test_prof/ext/float_duration.rb +14 -0
  35. data/lib/test_prof/factory_doctor/factory_girl_patch.rb +12 -0
  36. data/lib/test_prof/factory_doctor/minitest.rb +3 -0
  37. data/lib/test_prof/factory_doctor/rspec.rb +96 -0
  38. data/lib/test_prof/factory_doctor.rb +133 -0
  39. data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
  40. data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
  41. data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
  42. data/lib/test_prof/factory_prof.rb +140 -0
  43. data/lib/test_prof/logging.rb +25 -0
  44. data/lib/test_prof/recipes/rspec/any_fixture.rb +21 -0
  45. data/lib/test_prof/recipes/rspec/before_all.rb +23 -0
  46. data/lib/test_prof/rspec_stamp/parser.rb +103 -0
  47. data/lib/test_prof/rspec_stamp/rspec.rb +91 -0
  48. data/lib/test_prof/rspec_stamp.rb +135 -0
  49. data/lib/test_prof/rubocop.rb +3 -0
  50. data/lib/test_prof/ruby_prof/rspec.rb +13 -0
  51. data/lib/test_prof/ruby_prof.rb +194 -0
  52. data/lib/test_prof/stack_prof/rspec.rb +13 -0
  53. data/lib/test_prof/stack_prof.rb +120 -0
  54. data/lib/test_prof/version.rb +5 -0
  55. data/lib/test_prof.rb +108 -0
  56. 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: write Minitest reporter
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: write Minitest reporter
@@ -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