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.
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.
@@ -44,15 +44,9 @@ module TestProf
44
44
  defined?(Minitest)
45
45
  end
46
46
 
47
- # Avoid issues with wrong time due to monkey-patches (e.g. timecop)
48
- # See https://github.com/rspec/rspec-core/blob/v3.6.0/lib/rspec/core.rb#L147
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
@@ -17,7 +17,10 @@ module TestProf
17
17
 
18
18
  def begin_transaction
19
19
  raise AdapterMissing if adapter.nil?
20
- adapter.begin_transaction
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
- adapter.rollback_transaction
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
- module TestProf::EventProf::CustomEvents
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
- def track(factory)
23
- @depth += 1
24
- res = nil
25
- begin
26
- res =
27
- if @depth == 1
28
- ActiveSupport::Notifications.instrument(
29
- "factory.create",
30
- name: factory
31
- ) { yield }
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
- TestProf::EventProf::CustomEvents.register("factory.create") do
45
- if defined? TestProf::FactoryBot
46
- TestProf::EventProf::CustomEvents::FactoryCreate.setup!
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(:error,
49
- <<~MSG
50
- Failed to load factory_bot / factory_girl.
29
+ TestProf.log(
30
+ :error,
31
+ <<~MSG
32
+ Failed to load factory_bot / factory_girl / fabrication.
51
33
 
52
- Make sure that any of them is in your Gemfile.
53
- MSG
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::CustomEvents::SidekiqInline.setup!
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::CustomEvents::SidekiqJobs.setup!
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
- module_eval <<~SRC, __FILE__, __LINE__ + 1
12
- def #{mid}(*)
13
- TestProf::EventProf.instrumenter.instrument(
14
- '#{event}'
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(event = "sql.active_record")
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