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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -20
- data/README.md +10 -57
- data/assets/tagprof.demo.html +447 -0
- data/assets/tagprof.template.html +447 -0
- data/lib/minitest/event_prof_formatter.rb +18 -16
- data/lib/test_prof.rb +2 -1
- data/lib/test_prof/any_fixture.rb +80 -4
- data/lib/test_prof/any_fixture/dsl.rb +18 -0
- data/lib/test_prof/event_prof.rb +10 -108
- data/lib/test_prof/event_prof/custom_events.rb +30 -0
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +1 -1
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +1 -1
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +1 -1
- data/lib/test_prof/event_prof/minitest.rb +6 -0
- data/lib/test_prof/event_prof/profiler.rb +129 -0
- data/lib/test_prof/event_prof/rspec.rb +20 -11
- data/lib/test_prof/factory_all_stub.rb +32 -0
- data/lib/test_prof/factory_all_stub/factory_bot_patch.rb +13 -0
- data/lib/test_prof/factory_doctor/rspec.rb +3 -2
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +9 -13
- data/lib/test_prof/recipes/active_record_shared_connection.rb +55 -0
- data/lib/test_prof/recipes/logging.rb +37 -0
- data/lib/test_prof/recipes/rspec/any_fixture.rb +4 -1
- data/lib/test_prof/recipes/rspec/factory_all_stub.rb +10 -0
- data/lib/test_prof/rspec_dissect/rspec.rb +4 -2
- data/lib/test_prof/rspec_stamp/rspec.rb +3 -2
- data/lib/test_prof/tag_prof.rb +4 -0
- data/lib/test_prof/tag_prof/printers/html.rb +24 -0
- data/lib/test_prof/tag_prof/printers/simple.rb +82 -0
- data/lib/test_prof/tag_prof/result.rb +38 -0
- data/lib/test_prof/tag_prof/rspec.rb +43 -40
- data/lib/test_prof/utils/html_builder.rb +21 -0
- data/lib/test_prof/version.rb +1 -1
- metadata +20 -24
- data/assets/logo.svg +0 -1
- data/assets/testprof.png +0 -0
- data/guides/.rubocop.yml +0 -1
- data/guides/any_fixture.md +0 -114
- data/guides/before_all.md +0 -98
- data/guides/event_prof.md +0 -177
- data/guides/factory_default.md +0 -111
- data/guides/factory_doctor.md +0 -119
- data/guides/factory_prof.md +0 -86
- data/guides/let_it_be.md +0 -97
- data/guides/rspec_dissect.md +0 -60
- data/guides/rspec_stamp.md +0 -52
- data/guides/rubocop.md +0 -48
- data/guides/ruby_prof.md +0 -63
- data/guides/stack_prof.md +0 -47
- data/guides/tag_prof.md +0 -51
- data/guides/tests_sampling.md +0 -24
data/lib/test_prof.rb
CHANGED
@@ -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]
|
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
|
-
|
9
|
-
|
11
|
+
using FloatDuration
|
12
|
+
using StringStripHeredoc
|
10
13
|
|
11
|
-
|
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
|
-
|
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
|
data/lib/test_prof/event_prof.rb
CHANGED
@@ -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
|
76
|
-
def build
|
77
|
-
|
78
|
-
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.
|
47
|
+
TestProf::EventProf::CustomEvents.register("factory.create") do
|
48
48
|
if defined? TestProf::FactoryBot
|
49
49
|
TestProf::EventProf::CustomEvents::FactoryCreate.setup!
|
50
50
|
else
|
@@ -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
|