test-prof 0.4.9 → 0.5.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -20
  3. data/README.md +10 -57
  4. data/assets/tagprof.demo.html +447 -0
  5. data/assets/tagprof.template.html +447 -0
  6. data/lib/minitest/event_prof_formatter.rb +18 -16
  7. data/lib/test_prof.rb +2 -1
  8. data/lib/test_prof/any_fixture.rb +80 -4
  9. data/lib/test_prof/any_fixture/dsl.rb +18 -0
  10. data/lib/test_prof/event_prof.rb +10 -108
  11. data/lib/test_prof/event_prof/custom_events.rb +30 -0
  12. data/lib/test_prof/event_prof/custom_events/factory_create.rb +1 -1
  13. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +1 -1
  14. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +1 -1
  15. data/lib/test_prof/event_prof/minitest.rb +6 -0
  16. data/lib/test_prof/event_prof/profiler.rb +129 -0
  17. data/lib/test_prof/event_prof/rspec.rb +20 -11
  18. data/lib/test_prof/factory_all_stub.rb +32 -0
  19. data/lib/test_prof/factory_all_stub/factory_bot_patch.rb +13 -0
  20. data/lib/test_prof/factory_doctor/rspec.rb +3 -2
  21. data/lib/test_prof/factory_prof/printers/flamegraph.rb +9 -13
  22. data/lib/test_prof/recipes/active_record_shared_connection.rb +55 -0
  23. data/lib/test_prof/recipes/logging.rb +37 -0
  24. data/lib/test_prof/recipes/rspec/any_fixture.rb +4 -1
  25. data/lib/test_prof/recipes/rspec/factory_all_stub.rb +10 -0
  26. data/lib/test_prof/rspec_dissect/rspec.rb +4 -2
  27. data/lib/test_prof/rspec_stamp/rspec.rb +3 -2
  28. data/lib/test_prof/tag_prof.rb +4 -0
  29. data/lib/test_prof/tag_prof/printers/html.rb +24 -0
  30. data/lib/test_prof/tag_prof/printers/simple.rb +82 -0
  31. data/lib/test_prof/tag_prof/result.rb +38 -0
  32. data/lib/test_prof/tag_prof/rspec.rb +43 -40
  33. data/lib/test_prof/utils/html_builder.rb +21 -0
  34. data/lib/test_prof/version.rb +1 -1
  35. metadata +20 -24
  36. data/assets/logo.svg +0 -1
  37. data/assets/testprof.png +0 -0
  38. data/guides/.rubocop.yml +0 -1
  39. data/guides/any_fixture.md +0 -114
  40. data/guides/before_all.md +0 -98
  41. data/guides/event_prof.md +0 -177
  42. data/guides/factory_default.md +0 -111
  43. data/guides/factory_doctor.md +0 -119
  44. data/guides/factory_prof.md +0 -86
  45. data/guides/let_it_be.md +0 -97
  46. data/guides/rspec_dissect.md +0 -60
  47. data/guides/rspec_stamp.md +0 -52
  48. data/guides/rubocop.md +0 -48
  49. data/guides/ruby_prof.md +0 -63
  50. data/guides/stack_prof.md +0 -47
  51. data/guides/tag_prof.md +0 -51
  52. data/guides/tests_sampling.md +0 -24
@@ -90,7 +90,7 @@ module TestProf
90
90
  private
91
91
 
92
92
  def activate!(env_var, val)
93
- yield if ENV[env_var] && (val.nil? || ENV[env_var] == val)
93
+ yield if ENV[env_var] && (val.nil? || val === ENV[env_var])
94
94
  end
95
95
 
96
96
  def with_timestamps(path)
@@ -132,3 +132,4 @@ require "test_prof/factory_prof"
132
132
  require "test_prof/rspec_stamp"
133
133
  require "test_prof/tag_prof"
134
134
  require "test_prof/rspec_dissect"
135
+ require "test_prof/factory_all_stub"
@@ -1,27 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/float_duration"
4
+ require "test_prof/ext/string_strip_heredoc"
5
+
3
6
  module TestProf
4
7
  # Make DB fixtures from blocks.
5
8
  module AnyFixture
6
9
  INSERT_RXP = /^INSERT INTO ([\S]+)/
7
10
 
8
- class Cache # :nodoc:
9
- attr_reader :store
11
+ using FloatDuration
12
+ using StringStripHeredoc
10
13
 
11
- delegate :clear, to: :store
14
+ class Cache # :nodoc:
15
+ attr_reader :store, :stats
12
16
 
13
17
  def initialize
14
18
  @store = {}
19
+ @stats = {}
15
20
  end
16
21
 
17
22
  def fetch(key)
18
- return store[key] if store.key?(key)
23
+ if store.key?(key)
24
+ stats[key][:hit] += 1
25
+ return store[key]
26
+ end
27
+
19
28
  return unless block_given?
29
+
30
+ ts = TestProf.now
20
31
  store[key] = yield
32
+ stats[key] = { time: TestProf.now - ts, hit: 0 }
33
+ store[key]
34
+ end
35
+
36
+ def clear
37
+ store.clear
38
+ stats.clear
21
39
  end
22
40
  end
23
41
 
24
42
  class << self
43
+ include Logging
44
+
45
+ attr_accessor :reporting_enabled
46
+
47
+ def reporting_enabled?
48
+ reporting_enabled == true
49
+ end
50
+
25
51
  # Register a block of code as a fixture,
26
52
  # returns the result of the block execution
27
53
  def register(id)
@@ -53,6 +79,54 @@ module TestProf
53
79
  tables_cache[matches[1]] = true if matches
54
80
  end
55
81
 
82
+ def report_stats
83
+ msgs = []
84
+
85
+ msgs <<
86
+ <<-MSG.strip_heredoc
87
+ AnyFixture usage stats:
88
+ MSG
89
+
90
+ first_column = cache.stats.keys.map(&:size).max + 2
91
+
92
+ msgs << format(
93
+ "%#{first_column}s %12s %9s %12s",
94
+ 'key', 'build time', 'hit count', 'saved time'
95
+ )
96
+
97
+ msgs << ""
98
+
99
+ total_spent = 0.0
100
+ total_saved = 0.0
101
+ total_miss = 0.0
102
+
103
+ cache.stats.to_a.sort_by { |(_, v)| -v[:hit] }.each do |(key, stats)|
104
+ total_spent += stats[:time]
105
+
106
+ saved = stats[:time] * stats[:hit]
107
+
108
+ total_saved += saved
109
+
110
+ total_miss += stats[:time] if stats[:hit].zero?
111
+
112
+ msgs << format(
113
+ "%#{first_column}s %12s %9d %12s",
114
+ key, stats[:time].duration, stats[:hit],
115
+ saved.duration
116
+ )
117
+ end
118
+
119
+ msgs <<
120
+ <<-MSG.strip_heredoc
121
+
122
+ Total time spent: #{total_spent.duration}
123
+ Total time saved: #{total_saved.duration}
124
+ Total time wasted: #{total_miss.duration}
125
+ MSG
126
+
127
+ log :info, msgs.join("\n")
128
+ end
129
+
56
130
  private
57
131
 
58
132
  def cache
@@ -63,5 +137,7 @@ module TestProf
63
137
  @tables_cache ||= {}
64
138
  end
65
139
  end
140
+
141
+ self.reporting_enabled = ENV['ANYFIXTURE_REPORT'] == '1'
66
142
  end
67
143
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module AnyFixture
5
+ # Adds "global" `fixture` method (through refinement)
6
+ module DSL
7
+ module Ext # :nodoc:
8
+ def fixture(id, &block)
9
+ ::TestProf::AnyFixture.register(:"#{id}", &block)
10
+ end
11
+ end
12
+
13
+ refine Kernel do
14
+ include Ext
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/rspec_stamp"
4
+ require "test_prof/event_prof/profiler"
4
5
  require "test_prof/event_prof/instrumentations/active_support"
5
6
  require "test_prof/utils/sized_ordered_set"
6
7
 
@@ -72,119 +73,20 @@ module TestProf
72
73
  yield config
73
74
  end
74
75
 
75
- # Returns new configured instance of profiler
76
- def build
77
- Profiler.new(
78
- event: config.event,
79
- instrumenter: config.resolve_instrumenter
76
+ # Returns new configured instance of profilers group
77
+ def build(event = config.event)
78
+ ProfilersGroup.new(
79
+ event: event,
80
+ instrumenter: config.resolve_instrumenter,
81
+ rank_by: config.rank_by,
82
+ top_count: config.top_count,
83
+ per_example: config.per_example?
80
84
  )
81
85
  end
82
86
  end
83
-
84
- class Profiler # :nodoc:
85
- include TestProf::Logging
86
-
87
- attr_reader :event, :total_count, :total_time
88
-
89
- def initialize(event:, instrumenter:)
90
- @event = event
91
-
92
- log :info, "EventProf enabled (#{@event})"
93
-
94
- instrumenter.subscribe(event) { |time| track(time) }
95
-
96
- @groups = Utils::SizedOrderedSet.new(
97
- top_count, sort_by: rank_by
98
- )
99
-
100
- @examples = Utils::SizedOrderedSet.new(
101
- top_count, sort_by: rank_by
102
- )
103
-
104
- @total_count = 0
105
- @total_time = 0.0
106
- end
107
-
108
- def track(time)
109
- return if @current_group.nil?
110
- @total_time += time
111
- @total_count += 1
112
-
113
- @time += time
114
- @count += 1
115
-
116
- return if @current_example.nil?
117
-
118
- @example_time += time
119
- @example_count += 1
120
- end
121
-
122
- def group_started(id)
123
- reset_group!
124
- @current_group = id
125
- end
126
-
127
- def group_finished(id)
128
- data = { id: id, time: @time, count: @count, examples: @total_examples }
129
-
130
- @groups << data unless data[rank_by].zero?
131
-
132
- @current_group = nil
133
- end
134
-
135
- def example_started(id)
136
- return unless config.per_example?
137
- reset_example!
138
- @current_example = id
139
- end
140
-
141
- def example_finished(id)
142
- @total_examples += 1
143
- return unless config.per_example?
144
-
145
- data = { id: id, time: @example_time, count: @example_count }
146
- @examples << data unless data[rank_by].zero?
147
- @current_example = nil
148
- end
149
-
150
- def results
151
- {
152
- groups: @groups.to_a
153
- }.tap do |data|
154
- next unless config.per_example?
155
-
156
- data[:examples] = @examples.to_a
157
- end
158
- end
159
-
160
- def rank_by
161
- EventProf.config.rank_by
162
- end
163
-
164
- def top_count
165
- EventProf.config.top_count
166
- end
167
-
168
- private
169
-
170
- def config
171
- EventProf.config
172
- end
173
-
174
- def reset_group!
175
- @time = 0.0
176
- @count = 0
177
- @total_examples = 0
178
- end
179
-
180
- def reset_example!
181
- @example_count = 0
182
- @example_time = 0.0
183
- end
184
- end
185
87
  end
186
88
  end
187
89
 
90
+ require "test_prof/event_prof/custom_events"
188
91
  require "test_prof/event_prof/rspec" if defined?(RSpec::Core)
189
92
  require "test_prof/event_prof/minitest" if defined?(Minitest)
190
- require "test_prof/event_prof/custom_events"
@@ -1,5 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module TestProf
4
+ module EventProf
5
+ # Registers and activates custom events (which require patches).
6
+ module CustomEvents
7
+ class << self
8
+ def register(event)
9
+ raise ArgumentError, "Block is required!" unless block_given?
10
+ registrations[event] = Proc.new
11
+ end
12
+
13
+ def activate_all(events)
14
+ events = events.split(",")
15
+ events.each { |event| try_activate(event) }
16
+ end
17
+
18
+ def try_activate(event)
19
+ return unless registrations.key?(event)
20
+ registrations.delete(event).call
21
+ end
22
+
23
+ private
24
+
25
+ def registrations
26
+ @registrations ||= {}
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
3
33
  require "test_prof/event_prof/custom_events/factory_create"
4
34
  require "test_prof/event_prof/custom_events/sidekiq_inline"
5
35
  require "test_prof/event_prof/custom_events/sidekiq_jobs"
@@ -44,7 +44,7 @@ module TestProf::EventProf::CustomEvents
44
44
  end
45
45
  end
46
46
 
47
- TestProf.activate('EVENT_PROF', 'factory.create') do
47
+ TestProf::EventProf::CustomEvents.register("factory.create") do
48
48
  if defined? TestProf::FactoryBot
49
49
  TestProf::EventProf::CustomEvents::FactoryCreate.setup!
50
50
  else
@@ -40,7 +40,7 @@ module TestProf::EventProf::CustomEvents
40
40
  end
41
41
  end
42
42
 
43
- TestProf.activate('EVENT_PROF', 'sidekiq.inline') do
43
+ TestProf::EventProf::CustomEvents.register("sidekiq.inline") do
44
44
  if TestProf.require(
45
45
  'sidekiq/testing',
46
46
  <<-MSG.strip_heredoc
@@ -27,7 +27,7 @@ module TestProf::EventProf::CustomEvents
27
27
  end
28
28
  end
29
29
 
30
- TestProf.activate('EVENT_PROF', 'sidekiq.jobs') do
30
+ TestProf::EventProf::CustomEvents.register("sidekiq.jobs") do
31
31
  if TestProf.require(
32
32
  'sidekiq/testing',
33
33
  <<-MSG.strip_heredoc
@@ -9,6 +9,9 @@ module Minitest
9
9
  def initialize(io = $stdout, options = {})
10
10
  super
11
11
  @profiler = configure_profiler(options)
12
+
13
+ log :info, "EventProf enabled (#{@profiler.events.join(', ')})"
14
+
12
15
  @formatter = EventProfFormatter.new(@profiler)
13
16
  @current_group = nil
14
17
  @current_example = nil
@@ -64,7 +67,10 @@ module Minitest
64
67
  config.rank_by = options[:rank_by] if options[:rank_by]
65
68
  config.top_count = options[:top_count] if options[:top_count]
66
69
  config.per_example = options[:per_example] if options[:per_example]
70
+
71
+ ::TestProf::EventProf::CustomEvents.activate_all config.event
67
72
  end
73
+
68
74
  ::TestProf::EventProf.build
69
75
  end
70
76
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module EventProf
5
+ class Profiler # :nodoc:
6
+ attr_reader :event, :total_count, :total_time, :rank_by, :top_count, :per_example,
7
+ :time, :count, :example_time, :example_count
8
+
9
+ alias per_example? per_example
10
+
11
+ def initialize(event:, instrumenter:, rank_by: :time, top_count: 5, per_example: false)
12
+ @event = event
13
+ @rank_by = rank_by
14
+ @top_count = top_count
15
+ @per_example = per_example
16
+
17
+ instrumenter.subscribe(event) { |time| track(time) }
18
+
19
+ @groups = Utils::SizedOrderedSet.new(
20
+ top_count, sort_by: rank_by
21
+ )
22
+
23
+ @examples = Utils::SizedOrderedSet.new(
24
+ top_count, sort_by: rank_by
25
+ )
26
+
27
+ @total_count = 0
28
+ @total_time = 0.0
29
+ end
30
+
31
+ def track(time)
32
+ return if @current_group.nil?
33
+ @total_time += time
34
+ @total_count += 1
35
+
36
+ @time += time
37
+ @count += 1
38
+
39
+ return if @current_example.nil?
40
+
41
+ @example_time += time
42
+ @example_count += 1
43
+ end
44
+
45
+ def group_started(id)
46
+ reset_group!
47
+ @current_group = id
48
+ end
49
+
50
+ def group_finished(id)
51
+ data = { id: id, time: @time, count: @count, examples: @total_examples }
52
+
53
+ @groups << data unless data[rank_by].zero?
54
+
55
+ @current_group = nil
56
+ end
57
+
58
+ def example_started(id)
59
+ return unless per_example?
60
+ reset_example!
61
+ @current_example = id
62
+ end
63
+
64
+ def example_finished(id)
65
+ @total_examples += 1
66
+ return unless per_example?
67
+
68
+ data = { id: id, time: @example_time, count: @example_count }
69
+ @examples << data unless data[rank_by].zero?
70
+ @current_example = nil
71
+ end
72
+
73
+ def results
74
+ {
75
+ groups: @groups.to_a
76
+ }.tap do |data|
77
+ next unless per_example?
78
+
79
+ data[:examples] = @examples.to_a
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def reset_group!
86
+ @time = 0.0
87
+ @count = 0
88
+ @total_examples = 0
89
+ end
90
+
91
+ def reset_example!
92
+ @example_count = 0
93
+ @example_time = 0.0
94
+ end
95
+ end
96
+
97
+ # Multiple profilers wrapper
98
+ class ProfilersGroup
99
+ attr_reader :profilers
100
+
101
+ def initialize(event:, **options)
102
+ events = event.split(",")
103
+ @profilers = events.map do |ev|
104
+ Profiler.new(event: ev, **options)
105
+ end
106
+ end
107
+
108
+ def each
109
+ if block_given?
110
+ @profilers.each(&Proc.new)
111
+ else
112
+ @profilers.each
113
+ end
114
+ end
115
+
116
+ def events
117
+ @profilers.map(&:event)
118
+ end
119
+
120
+ %i[group_started group_finished example_started example_finished].each do |name|
121
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
122
+ def #{name}(id)
123
+ @profilers.each { |profiler| profiler.#{name}(id) }
124
+ end
125
+ CODE
126
+ end
127
+ end
128
+ end
129
+ end