test-prof 0.8.0 → 0.9.0
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 +4 -4
- data/CHANGELOG.md +156 -107
- data/LICENSE.txt +1 -1
- data/README.md +4 -0
- data/lib/test_prof.rb +3 -9
- data/lib/test_prof/before_all.rb +73 -2
- data/lib/test_prof/before_all/adapters/active_record.rb +11 -12
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +27 -45
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +7 -37
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +6 -24
- data/lib/test_prof/event_prof/monitor.rb +45 -8
- data/lib/test_prof/ext/factory_bot_strategy.rb +24 -0
- data/lib/test_prof/factory_doctor.rb +28 -6
- data/lib/test_prof/factory_doctor/fabrication_patch.rb +12 -0
- data/lib/test_prof/factory_doctor/factory_bot_patch.rb +5 -1
- data/lib/test_prof/factory_doctor/rspec.rb +8 -2
- data/lib/test_prof/factory_prof.rb +25 -11
- data/lib/test_prof/factory_prof/factory_builders/factory_bot.rb +2 -17
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +6 -2
- data/lib/test_prof/factory_prof/printers/simple.rb +8 -6
- data/lib/test_prof/recipes/rspec/let_it_be.rb +25 -0
- data/lib/test_prof/version.rb +1 -1
- metadata +13 -20
data/README.md
CHANGED
@@ -85,3 +85,7 @@ Already using TestProf? [Share your story!](https://github.com/palkan/test-prof/
|
|
85
85
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
86
86
|
|
87
87
|
[docs]: https://test-prof.evilmartians.io
|
88
|
+
|
89
|
+
## Security Contact
|
90
|
+
|
91
|
+
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
|
data/lib/test_prof.rb
CHANGED
@@ -44,15 +44,9 @@ module TestProf
|
|
44
44
|
defined?(Minitest)
|
45
45
|
end
|
46
46
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
# We also want to handle Timecop specificaly
|
51
|
-
# See https://github.com/travisjeffery/timecop/blob/master/lib/timecop/time_extensions.rb#L11
|
52
|
-
if Time.respond_to?(:now_without_mock_time)
|
53
|
-
define_method(:now, &::Time.method(:now_without_mock_time))
|
54
|
-
else
|
55
|
-
define_method(:now, &::Time.method(:now))
|
47
|
+
# Returns the current process time
|
48
|
+
def now
|
49
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
56
50
|
end
|
57
51
|
|
58
52
|
# Require gem and shows a custom
|
data/lib/test_prof/before_all.rb
CHANGED
@@ -17,7 +17,10 @@ module TestProf
|
|
17
17
|
|
18
18
|
def begin_transaction
|
19
19
|
raise AdapterMissing if adapter.nil?
|
20
|
-
|
20
|
+
|
21
|
+
config.run_hooks(:begin) do
|
22
|
+
adapter.begin_transaction
|
23
|
+
end
|
21
24
|
yield
|
22
25
|
end
|
23
26
|
|
@@ -27,8 +30,76 @@ module TestProf
|
|
27
30
|
|
28
31
|
def rollback_transaction
|
29
32
|
raise AdapterMissing if adapter.nil?
|
30
|
-
|
33
|
+
|
34
|
+
config.run_hooks(:rollback) do
|
35
|
+
adapter.rollback_transaction
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def config
|
40
|
+
@config ||= Configuration.new
|
31
41
|
end
|
42
|
+
|
43
|
+
def configure
|
44
|
+
yield config
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class HooksChain # :nodoc:
|
49
|
+
attr_reader :type, :after, :before
|
50
|
+
|
51
|
+
def initialize(type)
|
52
|
+
@type = type
|
53
|
+
@before = []
|
54
|
+
@after = []
|
55
|
+
end
|
56
|
+
|
57
|
+
def run
|
58
|
+
before.each(&:call)
|
59
|
+
yield
|
60
|
+
after.each(&:call)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Configuration
|
65
|
+
HOOKS = %i[begin rollback].freeze
|
66
|
+
|
67
|
+
def initialize
|
68
|
+
@hooks = Hash.new { |h, k| h[k] = HooksChain.new(k) }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Add `before` hook for `begin` or
|
72
|
+
# `rollback` operation:
|
73
|
+
#
|
74
|
+
# config.before(:rollback) { ... }
|
75
|
+
def before(type)
|
76
|
+
validate_hook_type!(type)
|
77
|
+
hooks[type].before << Proc.new
|
78
|
+
end
|
79
|
+
|
80
|
+
# Add `after` hook for `begin` or
|
81
|
+
# `rollback` operation:
|
82
|
+
#
|
83
|
+
# config.after(:begin) { ... }
|
84
|
+
def after(type)
|
85
|
+
validate_hook_type!(type)
|
86
|
+
hooks[type].after << Proc.new
|
87
|
+
end
|
88
|
+
|
89
|
+
def run_hooks(type) # :nodoc:
|
90
|
+
validate_hook_type!(type)
|
91
|
+
hooks[type].run { yield }
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def validate_hook_type!(type)
|
97
|
+
return if HOOKS.include?(type)
|
98
|
+
|
99
|
+
raise ArgumentError, "Unknown hook type: #{type}. Valid types: #{HOOKS.join(", ")}"
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :hooks
|
32
103
|
end
|
33
104
|
end
|
34
105
|
end
|
@@ -12,7 +12,6 @@ module TestProf
|
|
12
12
|
module ActiveRecord
|
13
13
|
class << self
|
14
14
|
def begin_transaction
|
15
|
-
lock_thread!
|
16
15
|
::ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
17
16
|
end
|
18
17
|
|
@@ -24,19 +23,19 @@ module TestProf
|
|
24
23
|
end
|
25
24
|
::ActiveRecord::Base.connection.rollback_transaction
|
26
25
|
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
# Make sure ActiveRecord uses locked thread.
|
31
|
-
# It only gets locked in `before` / `setup` hook,
|
32
|
-
# thus using thread in `before_all` (e.g. ActiveJob async adapter)
|
33
|
-
# might lead to leaking connections
|
34
|
-
def lock_thread!
|
35
|
-
return unless ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
|
36
|
-
::ActiveRecord::Base.connection.pool.lock_thread = true
|
37
|
-
end
|
38
26
|
end
|
39
27
|
end
|
40
28
|
end
|
29
|
+
|
30
|
+
configure do |config|
|
31
|
+
# Make sure ActiveRecord uses locked thread.
|
32
|
+
# It only gets locked in `before` / `setup` hook,
|
33
|
+
# thus using thread in `before_all` (e.g. ActiveJob async adapter)
|
34
|
+
# might lead to leaking connections
|
35
|
+
config.before(:begin) do
|
36
|
+
next unless ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
|
37
|
+
::ActiveRecord::Base.connection.pool.lock_thread = true
|
38
|
+
end
|
39
|
+
end
|
41
40
|
end
|
42
41
|
end
|
@@ -1,56 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "test_prof/factory_bot"
|
4
|
+
require "test_prof/ext/factory_bot_strategy"
|
4
5
|
|
5
|
-
|
6
|
-
module FactoryCreate # :nodoc: all
|
7
|
-
module RunnerPatch
|
8
|
-
def run(strategy = @strategy)
|
9
|
-
return super unless strategy == :create
|
10
|
-
FactoryCreate.track(@name) do
|
11
|
-
super
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
class << self
|
17
|
-
def setup!
|
18
|
-
@depth = 0
|
19
|
-
TestProf::FactoryBot::FactoryRunner.prepend RunnerPatch
|
20
|
-
end
|
6
|
+
using TestProf::FactoryBotStrategy
|
21
7
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
else
|
33
|
-
yield
|
34
|
-
end
|
35
|
-
ensure
|
36
|
-
@depth -= 1
|
37
|
-
end
|
38
|
-
res
|
39
|
-
end
|
8
|
+
TestProf::EventProf::CustomEvents.register("factory.create") do
|
9
|
+
if defined?(TestProf::FactoryBot) || defined?(Fabricate)
|
10
|
+
if defined?(TestProf::FactoryBot)
|
11
|
+
TestProf::EventProf.monitor(
|
12
|
+
TestProf::FactoryBot::FactoryRunner,
|
13
|
+
"factory.create",
|
14
|
+
:run,
|
15
|
+
top_level: true,
|
16
|
+
guard: ->(strategy = @strategy) { strategy.create? }
|
17
|
+
)
|
40
18
|
end
|
41
|
-
end
|
42
|
-
end
|
43
19
|
|
44
|
-
|
45
|
-
|
46
|
-
|
20
|
+
if defined?(Fabricate)
|
21
|
+
TestProf::EventProf.monitor(
|
22
|
+
Fabricate.singleton_class,
|
23
|
+
"factory.create",
|
24
|
+
:create,
|
25
|
+
top_level: true
|
26
|
+
)
|
27
|
+
end
|
47
28
|
else
|
48
|
-
TestProf.log(
|
49
|
-
|
50
|
-
|
29
|
+
TestProf.log(
|
30
|
+
:error,
|
31
|
+
<<~MSG
|
32
|
+
Failed to load factory_bot / factory_girl / fabrication.
|
51
33
|
|
52
|
-
|
53
|
-
|
54
|
-
|
34
|
+
Make sure that any of them is in your Gemfile.
|
35
|
+
MSG
|
36
|
+
)
|
55
37
|
end
|
56
38
|
end
|
@@ -1,41 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module TestProf::EventProf::CustomEvents
|
4
|
-
module SidekiqInline # :nodoc: all
|
5
|
-
module ClientPatch
|
6
|
-
def raw_push(*)
|
7
|
-
return super unless Sidekiq::Testing.inline?
|
8
|
-
SidekiqInline.track { super }
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
class << self
|
13
|
-
def setup!
|
14
|
-
@depth = 0
|
15
|
-
Sidekiq::Client.prepend ClientPatch
|
16
|
-
end
|
17
|
-
|
18
|
-
def track
|
19
|
-
@depth += 1
|
20
|
-
res = nil
|
21
|
-
begin
|
22
|
-
res =
|
23
|
-
if @depth == 1
|
24
|
-
ActiveSupport::Notifications.instrument(
|
25
|
-
"sidekiq.inline"
|
26
|
-
) { yield }
|
27
|
-
else
|
28
|
-
yield
|
29
|
-
end
|
30
|
-
ensure
|
31
|
-
@depth -= 1
|
32
|
-
end
|
33
|
-
res
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
3
|
TestProf::EventProf::CustomEvents.register("sidekiq.inline") do
|
40
4
|
if TestProf.require(
|
41
5
|
"sidekiq/testing",
|
@@ -45,6 +9,12 @@ TestProf::EventProf::CustomEvents.register("sidekiq.inline") do
|
|
45
9
|
Make sure that "sidekiq" gem is in your Gemfile.
|
46
10
|
MSG
|
47
11
|
)
|
48
|
-
TestProf::EventProf
|
12
|
+
TestProf::EventProf.monitor(
|
13
|
+
Sidekiq::Client,
|
14
|
+
"sidekiq.inline",
|
15
|
+
:raw_push,
|
16
|
+
top_level: true,
|
17
|
+
guard: ->(*) { Sidekiq::Testing.inline? }
|
18
|
+
)
|
49
19
|
end
|
50
20
|
end
|
@@ -1,28 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
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
3
|
TestProf::EventProf::CustomEvents.register("sidekiq.jobs") do
|
27
4
|
if TestProf.require(
|
28
5
|
"sidekiq/testing",
|
@@ -32,7 +9,12 @@ TestProf::EventProf::CustomEvents.register("sidekiq.jobs") do
|
|
32
9
|
Make sure that "sidekiq" gem is in your Gemfile.
|
33
10
|
MSG
|
34
11
|
)
|
35
|
-
TestProf::EventProf
|
12
|
+
TestProf::EventProf.monitor(
|
13
|
+
Sidekiq::Client,
|
14
|
+
"sidekiq.jobs",
|
15
|
+
:raw_push,
|
16
|
+
guard: ->(*) { Sidekiq::Testing.inline? }
|
17
|
+
)
|
36
18
|
TestProf::EventProf.configure do |config|
|
37
19
|
config.rank_by = :count
|
38
20
|
end
|
@@ -4,17 +4,54 @@ module TestProf
|
|
4
4
|
module EventProf
|
5
5
|
# Wrap methods with instrumentation
|
6
6
|
module Monitor
|
7
|
+
class BaseTracker
|
8
|
+
attr_reader :event
|
9
|
+
|
10
|
+
def initialize(event)
|
11
|
+
@event = event
|
12
|
+
end
|
13
|
+
|
14
|
+
def track
|
15
|
+
TestProf::EventProf.instrumenter.instrument(event) { yield }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class TopLevelTracker < BaseTracker
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
def initialize(event)
|
23
|
+
super
|
24
|
+
@id = :"event_prof_monitor_#{event}"
|
25
|
+
Thread.current[id] = 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def track
|
29
|
+
Thread.current[id] += 1
|
30
|
+
res = nil
|
31
|
+
begin
|
32
|
+
res =
|
33
|
+
if Thread.current[id] == 1
|
34
|
+
super { yield }
|
35
|
+
else
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
ensure
|
39
|
+
Thread.current[id] -= 1
|
40
|
+
end
|
41
|
+
res
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
7
45
|
class << self
|
8
|
-
def call(mod, event, *mids)
|
46
|
+
def call(mod, event, *mids, guard: nil, top_level: false)
|
47
|
+
tracker = top_level ? TopLevelTracker.new(event) : BaseTracker.new(event)
|
48
|
+
|
9
49
|
patch = Module.new do
|
10
50
|
mids.each do |mid|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
) { super }
|
16
|
-
end
|
17
|
-
SRC
|
51
|
+
define_method(mid) do |*args, &block|
|
52
|
+
next super(*args, &block) unless guard.nil? || instance_exec(*args, &guard)
|
53
|
+
tracker.track { super(*args, &block) }
|
54
|
+
end
|
18
55
|
end
|
19
56
|
end
|
20
57
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
# FactoryBot 5.0 uses strategy classes for associations,
|
5
|
+
# older versions and top-level invocations use Symbols.
|
6
|
+
#
|
7
|
+
# This Refinement should be used FactoryRunner patches to check
|
8
|
+
# that strategy is :create.
|
9
|
+
module FactoryBotStrategy
|
10
|
+
refine Symbol do
|
11
|
+
def create?
|
12
|
+
self == :create
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if defined?(::FactoryBot::Strategy::Create)
|
17
|
+
refine Class do
|
18
|
+
def create?
|
19
|
+
self <= ::FactoryBot::Strategy::Create
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "test_prof"
|
4
4
|
require "test_prof/factory_bot"
|
5
5
|
require "test_prof/factory_doctor/factory_bot_patch"
|
6
|
+
require "test_prof/factory_doctor/fabrication_patch"
|
6
7
|
|
7
8
|
module TestProf
|
8
9
|
# FactoryDoctor is a tool that helps you identify
|
@@ -18,7 +19,7 @@ module TestProf
|
|
18
19
|
end
|
19
20
|
|
20
21
|
def bad?
|
21
|
-
count > 0 && queries_count.zero?
|
22
|
+
count > 0 && queries_count.zero? && time >= FactoryDoctor.config.threshold
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
@@ -38,23 +39,44 @@ module TestProf
|
|
38
39
|
\ASAVEPOINT
|
39
40
|
)}xi.freeze
|
40
41
|
|
42
|
+
class Configuration
|
43
|
+
attr_accessor :event, :threshold
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
# event to track for DB interactions
|
47
|
+
@event = ENV.fetch("FDOC_EVENT", "sql.active_record")
|
48
|
+
# consider result good if time wasted less then threshold
|
49
|
+
@threshold = ENV.fetch("FDOC_THRESHOLD", "0.01").to_f
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
41
53
|
class << self
|
42
54
|
include TestProf::Logging
|
43
55
|
|
44
|
-
attr_reader :event
|
45
56
|
attr_reader :count, :time, :queries_count
|
46
57
|
|
58
|
+
def config
|
59
|
+
@config ||= Configuration.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def configure
|
63
|
+
yield config
|
64
|
+
end
|
65
|
+
|
47
66
|
# Patch factory lib, init counters
|
48
|
-
def init
|
49
|
-
@event = event
|
67
|
+
def init
|
50
68
|
reset!
|
51
69
|
|
52
|
-
log :info, "FactoryDoctor enabled"
|
70
|
+
log :info, "FactoryDoctor enabled (event: \"#{config.event}\", threshold: #{config.threshold})"
|
53
71
|
|
54
72
|
# Monkey-patch FactoryBot / FactoryGirl
|
55
73
|
TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
|
56
74
|
defined?(TestProf::FactoryBot)
|
57
75
|
|
76
|
+
# Monkey-patch Fabrication
|
77
|
+
::Fabricate.singleton_class.prepend(FabricationPatch) if
|
78
|
+
defined?(::Fabricate)
|
79
|
+
|
58
80
|
subscribe!
|
59
81
|
|
60
82
|
@stamp = ENV["FDOC_STAMP"]
|
@@ -122,7 +144,7 @@ module TestProf
|
|
122
144
|
end
|
123
145
|
|
124
146
|
def subscribe!
|
125
|
-
::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
|
147
|
+
::ActiveSupport::Notifications.subscribe(config.event) do |_name, _start, _finish, _id, query|
|
126
148
|
next if ignore? || !running? || within_factory?
|
127
149
|
next if query[:sql] =~ IGNORED_QUERIES_PATTERN
|
128
150
|
@queries_count += 1
|