test-prof 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|