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.
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