test-prof 1.3.3.1 → 1.4.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60840c0d3e625673d2fc56a9d66f9581a1730dc137c31b4b7f00fe6791cf316e
4
- data.tar.gz: 0a00ada331651604776b8f310c35344de860447953b43aab1a1d14437854c8dc
3
+ metadata.gz: 2bb23a2e0630d64b02f4f755f801189a19a5d26179dcb5d9d9420b73a0b78319
4
+ data.tar.gz: 56fe8596860ac93bb46adb6d6c9ff027de6eddf43eb3fbd28d69f2fddd76c1eb
5
5
  SHA512:
6
- metadata.gz: 5d94cb3a4de94a249566ad0042bbff4cf0a8a855b291a54ef5233c33546d82e19cf149a636c2543574aa5af84dce9688484876ffdfd80715e8e47ac1b0470145
7
- data.tar.gz: 777fc797f1dd4998c8859c985a303644e2c807afbc8cc4c94f557d29145bc48d9a3e44a16b062798fc6e32793fd8c7beb2f43f95d622d35bd86eb52b0bc96a72
6
+ metadata.gz: 03f0304518182b46058f20f2a818067f264901278da60f0449cbc9ca37e3283bd093e737f7dff4312b3e4e99e3f24106d620ba90c6561b6218b3f99077785d53
7
+ data.tar.gz: 37608d5a827f5caff39f1fdd0ad066fed97d9cf42c1da532dac6b0019f3372f89b0779daf4ad3e570435c3aea624789d61723c6155c147e9d140d27d9db5f071
data/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ - Drop support for **Ruby <2.7** and **Rails <6**.
6
+
7
+ - FactoryDefault: Add `#get_factory_default`. [[@john-h-k][]]
8
+
9
+ - Add variations information to FactorProf reports. ([@lHydra][])
10
+
11
+ Get info on traits/overrides used by running `FPROF=1 FPROF_VARS=1 <your test command>`.
12
+
13
+ - Add support for `report_duplicates` config option for `let_it_be` ([@lHydra][])
14
+
15
+ - Support latest Timecop patching `Process.clock_gettime`. ([@palkan][])
16
+
17
+ - Vernier: Add hooks configuration parameter. ([@lHydra][])
18
+
19
+ Now you can add more insights to the resulting report by adding event markers from Active Support Notifications.
20
+ To do this, specify the `TEST_VERNIER_HOOKS=rails` env var or set it through `Vernier` configuration:
21
+
22
+ ```ruby
23
+ TestProf::Vernier.configure do |config|
24
+ config.hooks = :rails
25
+ end
26
+ ```
27
+
28
+ - FactoryProf: Add threshold configuration parameter. ([@lHydra][])
29
+
30
+ Now you can ignore factories which total number of calls is less than the provided threshold. To do this, specify
31
+ the `FPROF_THRESHOLD=30` env var or set it through `FactoryProf` configuration:
32
+
33
+ ```ruby
34
+ TestProf::FactoryProf.configure do |config|
35
+ config.threshold = 30
36
+ end
37
+ ```
38
+
5
39
  ## 1.3.3 (2024-04-19)
6
40
 
7
41
  - Fix MemProf bugs. ([@palkan][])
@@ -390,3 +424,5 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
390
424
  [@Vankiru]: https://github.com/Vankiru
391
425
  [@uzushino]: https://github.com/uzushino
392
426
  [@lioneldebauge]: https://github.com/lioneldebauge
427
+ [@lHydra]: https://github.com/lHydra
428
+ [@john-h-k]: https://github.com/john-h-k
data/README.md CHANGED
@@ -83,9 +83,9 @@ And that's it)
83
83
 
84
84
  Supported Ruby versions:
85
85
 
86
- - Ruby (MRI) >= 2.5.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0, Ruby 2.3 use TestProf ~> 0.7.0, Ruby 2.4 use TestProf <0.12.0)
86
+ - Ruby (MRI) >= 2.7.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0, Ruby 2.3 use TestProf ~> 0.7.0, Ruby 2.4 use TestProf <0.12.0, Ruby 2.5-2.6 use TestProf < 1.3)
87
87
 
88
- - JRuby >= 9.1.0.0 (**NOTE:** refinements-dependent features might require 9.2.7+)
88
+ - JRuby >= 9.3.0
89
89
 
90
90
  Supported RSpec version (for RSpec features only): >= 3.5.0 (for older RSpec versions use TestProf < 0.8.0).
91
91
 
@@ -8,44 +8,58 @@ module TestProf
8
8
  POOL_ARGS = ((::ActiveRecord::VERSION::MAJOR > 6) ? [:writing] : []).freeze
9
9
 
10
10
  class << self
11
- def all_connections
12
- @all_connections ||= if ::ActiveRecord::Base.respond_to? :connects_to
13
- ::ActiveRecord::Base.connection_handler.connection_pool_list(*POOL_ARGS).map { |pool|
14
- begin
15
- pool.connection
16
- rescue *pool_connection_errors => error
17
- log_pool_connection_error(pool, error)
18
- nil
19
- end
20
- }.compact
21
- else
22
- Array.wrap(::ActiveRecord::Base.connection)
11
+ if ::ActiveRecord::Base.connection.pool.respond_to?(:pin_connection!)
12
+ def begin_transaction
13
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:writing).each do |pool|
14
+ pool.pin_connection!(true)
15
+ end
23
16
  end
24
- end
25
17
 
26
- def pool_connection_errors
27
- @pool_connection_errors ||= []
28
- end
18
+ def rollback_transaction
19
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:writing).each do |pool|
20
+ pool.unpin_connection!
21
+ end
22
+ end
23
+ else
24
+ def all_connections
25
+ @all_connections ||= if ::ActiveRecord::Base.respond_to? :connects_to
26
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(*POOL_ARGS).filter_map { |pool|
27
+ begin
28
+ pool.connection
29
+ rescue *pool_connection_errors => error
30
+ log_pool_connection_error(pool, error)
31
+ nil
32
+ end
33
+ }
34
+ else
35
+ Array.wrap(::ActiveRecord::Base.connection)
36
+ end
37
+ end
29
38
 
30
- def log_pool_connection_error(pool, error)
31
- warn "Could not connect to pool #{pool.connection_class.name}. #{error.class}: #{error.message}"
32
- end
39
+ def pool_connection_errors
40
+ @pool_connection_errors ||= []
41
+ end
33
42
 
34
- def begin_transaction
35
- @all_connections = nil
36
- all_connections.each do |connection|
37
- connection.begin_transaction(joinable: false)
43
+ def log_pool_connection_error(pool, error)
44
+ warn "Could not connect to pool #{pool.connection_class.name}. #{error.class}: #{error.message}"
38
45
  end
39
- end
40
46
 
41
- def rollback_transaction
42
- all_connections.each do |connection|
43
- if connection.open_transactions.zero?
44
- warn "!!! before_all transaction has been already rollbacked and " \
45
- "could work incorrectly"
46
- next
47
+ def begin_transaction
48
+ @all_connections = nil
49
+ all_connections.each do |connection|
50
+ connection.begin_transaction(joinable: false)
51
+ end
52
+ end
53
+
54
+ def rollback_transaction
55
+ all_connections.each do |connection|
56
+ if connection.open_transactions.zero?
57
+ warn "!!! before_all transaction has been already rollbacked and " \
58
+ "could work incorrectly"
59
+ next
60
+ end
61
+ connection.rollback_transaction
47
62
  end
48
- connection.rollback_transaction
49
63
  end
50
64
  end
51
65
 
@@ -67,23 +81,23 @@ module TestProf
67
81
  end
68
82
  end
69
83
 
70
- # avoid instance variable collisions with cats
71
- PREFIX_RESTORE_LOCK_THREAD = "@😺"
84
+ unless ::ActiveRecord::Base.connection.pool.respond_to?(:pin_connection!)
85
+ # avoid instance variable collisions with cats
86
+ PREFIX_RESTORE_LOCK_THREAD = "@😺"
72
87
 
73
- configure do |config|
74
- # Make sure ActiveRecord uses locked thread.
75
- # It only gets locked in `before` / `setup` hook,
76
- # thus using thread in `before_all` (e.g. ActiveJob async adapter)
77
- # might lead to leaking connections
78
- config.before(:begin) do
79
- next unless ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
80
- instance_variable_set("#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread", ::ActiveRecord::Base.connection.pool.instance_variable_get(:@lock_thread)) unless instance_variable_defined? "#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread"
81
- ::ActiveRecord::Base.connection.pool.lock_thread = true
82
- end
88
+ configure do |config|
89
+ # Make sure ActiveRecord uses locked thread.
90
+ # It only gets locked in `before` / `setup` hook,
91
+ # thus using thread in `before_all` (e.g. ActiveJob async adapter)
92
+ # might lead to leaking connections
93
+ config.before(:begin) do
94
+ instance_variable_set("#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread", ::ActiveRecord::Base.connection.pool.instance_variable_get(:@lock_thread)) unless instance_variable_defined? "#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread"
95
+ ::ActiveRecord::Base.connection.pool.lock_thread = true
96
+ end
83
97
 
84
- config.after(:rollback) do
85
- next unless ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
86
- ::ActiveRecord::Base.connection.pool.lock_thread = instance_variable_get("#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread")
98
+ config.after(:rollback) do
99
+ ::ActiveRecord::Base.connection.pool.lock_thread = instance_variable_get("#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread")
100
+ end
87
101
  end
88
102
  end
89
103
  end
@@ -6,6 +6,29 @@ require "logger"
6
6
  require "test_prof/logging"
7
7
  require "test_prof/utils"
8
8
 
9
+ # Add an alias for Process.clock_gettime "reserved" for TestProf
10
+ # (in case some other tool would like to patch it)
11
+ module ::Process
12
+ class << self
13
+ # Already patched by Timecop
14
+ if method_defined?(:clock_gettime_without_mock)
15
+ alias_method :clock_gettime_for_test_prof, :clock_gettime_without_mock
16
+ else
17
+ alias_method :clock_gettime_for_test_prof, :clock_gettime
18
+
19
+ def singleton_method_added(method_name)
20
+ return super unless method_name == :clock_gettime_without_mock
21
+
22
+ define_method(:clock_gettime_for_test_prof) { |*args| clock_gettime_without_mock(*args) }
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Main TestProf module
29
+ #
30
+ # Contains configuration and common methods
31
+
9
32
  # Ruby applications tests profiling tools.
10
33
  #
11
34
  # Contains tools to analyze factories usage, integrate with Ruby profilers,
@@ -54,7 +77,7 @@ module TestProf
54
77
 
55
78
  # Returns the current process time
56
79
  def now
57
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
80
+ Process.clock_gettime_for_test_prof(Process::CLOCK_MONOTONIC)
58
81
  end
59
82
 
60
83
  # Require gem and shows a custom
@@ -48,16 +48,9 @@ module TestProf
48
48
 
49
49
  patch = Module.new do
50
50
  mids.each do |mid|
51
- if RUBY_VERSION >= "2.7.0"
52
- define_method(mid) do |*args, **kwargs, &block|
53
- next super(*args, **kwargs, &block) unless guard.nil? || instance_exec(*args, **kwargs, &guard)
54
- tracker.track { super(*args, **kwargs, &block) }
55
- end
56
- else
57
- define_method(mid) do |*args, &block|
58
- next super(*args, &block) unless guard.nil? || instance_exec(*args, &guard)
59
- tracker.track { super(*args, &block) }
60
- end
51
+ define_method(mid) do |*args, **kwargs, &block|
52
+ next super(*args, **kwargs, &block) unless guard.nil? || instance_exec(*args, **kwargs, &guard)
53
+ tracker.track { super(*args, **kwargs, &block) }
61
54
  end
62
55
  end
63
56
  end
@@ -3,6 +3,8 @@
3
3
  module TestProf # :nodoc: all
4
4
  FACTORY_GIRL_NAMES = {"factory_bot" => "::FactoryBot", "factory_girl" => "::FactoryGirl"}.freeze
5
5
 
6
+ TestProf.require("active_support/inflector")
7
+
6
8
  FACTORY_GIRL_NAMES.find do |name, cname|
7
9
  TestProf.require(name) do
8
10
  TestProf::FactoryBot = Object.const_get(cname)
@@ -12,7 +12,7 @@ module TestProf
12
12
 
13
13
  module StrategyExt
14
14
  def association(runner)
15
- FactoryDefault.get(runner.name, runner.traits, runner.overrides) ||
15
+ FactoryDefault.get(runner.name, runner.traits, runner.overrides, **{}) ||
16
16
  FactoryDefault.profiler.instrument(runner.name, runner.traits, runner.overrides) { super }
17
17
  end
18
18
  end
@@ -159,7 +159,8 @@ module TestProf
159
159
  set_factory_default(name, obj, **default_options)
160
160
  end
161
161
 
162
- def set_factory_default(name, obj, preserve_traits: FactoryDefault.config.preserve_traits, preserve_attributes: FactoryDefault.config.preserve_attributes, **other)
162
+ def set_factory_default(*name, obj, preserve_traits: FactoryDefault.config.preserve_traits, preserve_attributes: FactoryDefault.config.preserve_attributes, **other)
163
+ name = name.first if name.size == 1
163
164
  FactoryDefault.register(
164
165
  name, obj,
165
166
  preserve_traits: preserve_traits,
@@ -168,6 +169,10 @@ module TestProf
168
169
  )
169
170
  end
170
171
 
172
+ def get_factory_default(name, *traits, **overrides)
173
+ FactoryDefault.get(name, traits, overrides, skip_stats: true)
174
+ end
175
+
171
176
  def skip_factory_default(&block)
172
177
  FactoryDefault.disable!(&block)
173
178
  end
@@ -236,7 +241,7 @@ module TestProf
236
241
  obj
237
242
  end
238
243
 
239
- def get(name, traits = nil, overrides = nil)
244
+ def get(name, traits = nil, overrides = nil, skip_stats: false)
240
245
  return unless enabled?
241
246
 
242
247
  record = store[name]
@@ -248,7 +253,7 @@ module TestProf
248
253
  traits = nil
249
254
  end
250
255
 
251
- stats[name][:miss] += 1
256
+ stats[name][:miss] += 1 unless skip_stats
252
257
 
253
258
  if traits && !traits.empty? && record[:preserve_traits]
254
259
  return
@@ -263,8 +268,10 @@ module TestProf
263
268
  end
264
269
  end
265
270
 
266
- stats[name][:miss] -= 1
267
- stats[name][:hit] += 1
271
+ unless skip_stats
272
+ stats[name][:miss] -= 1
273
+ stats[name][:hit] += 1
274
+ end
268
275
 
269
276
  if record[:context] && (record[:context] != :example)
270
277
  object.refind
@@ -5,7 +5,13 @@ module TestProf
5
5
  # Wrap #run method with FactoryProf tracking
6
6
  module FabricationPatch
7
7
  def create(name, overrides = {})
8
- FactoryBuilders::Fabrication.track(name) { super }
8
+ variation = ""
9
+
10
+ if FactoryProf.config.include_variations && !overrides.empty?
11
+ variation += overrides.keys.sort.to_s.gsub(/[\\":]/, "")
12
+ end
13
+
14
+ FactoryBuilders::Fabrication.track(name, variation: variation.to_sym) { super }
9
15
  end
10
16
  end
11
17
  end
@@ -5,7 +5,21 @@ module TestProf
5
5
  # Wrap #run method with FactoryProf tracking
6
6
  module FactoryBotPatch
7
7
  def run(strategy = @strategy)
8
- FactoryBuilders::FactoryBot.track(strategy, @name) { super }
8
+ variation = ""
9
+
10
+ if FactoryProf.config.include_variations
11
+ if @traits || @overrides
12
+ unless @traits.empty?
13
+ variation += @traits.sort.join(".").prepend(".")
14
+ end
15
+
16
+ unless @overrides.empty?
17
+ variation += @overrides.keys.sort.to_s.gsub(/[\\":]/, "")
18
+ end
19
+ end
20
+ end
21
+
22
+ FactoryBuilders::FactoryBot.track(strategy, @name, variation: variation.to_sym) { super }
9
23
  end
10
24
  end
11
25
  end
@@ -15,8 +15,8 @@ module TestProf
15
15
  end
16
16
  end
17
17
 
18
- def self.track(factory, &block)
19
- FactoryProf.track(factory, &block)
18
+ def self.track(factory, **opts, &block)
19
+ FactoryProf.track(factory, **opts, &block)
20
20
  end
21
21
  end
22
22
  end
@@ -18,9 +18,9 @@ module TestProf
18
18
  defined? TestProf::FactoryBot
19
19
  end
20
20
 
21
- def self.track(strategy, factory, &block)
21
+ def self.track(strategy, factory, **opts, &block)
22
22
  return yield unless strategy.create?
23
- FactoryProf.track(factory, &block)
23
+ FactoryProf.track(factory, **opts, &block)
24
24
  end
25
25
  end
26
26
  end
@@ -9,7 +9,7 @@ module TestProf::FactoryProf
9
9
  using TestProf::FloatDuration
10
10
  include TestProf::Logging
11
11
 
12
- def dump(result, start_time:)
12
+ def dump(result, start_time:, **)
13
13
  return log(:info, "No factories detected") if result.raw_stats == {}
14
14
 
15
15
  outpath = TestProf.artifact_path("test-prof.result.json")
@@ -10,7 +10,7 @@ module TestProf::FactoryProf
10
10
  using TestProf::FloatDuration
11
11
  include TestProf::Logging
12
12
 
13
- def dump(result, start_time:)
13
+ def dump(result, start_time:, **)
14
14
  return if result.raw_stats == {}
15
15
 
16
16
  total_time = result.stats.sum { |stat| stat[:top_level_time] }
@@ -9,7 +9,7 @@ module TestProf::FactoryProf
9
9
  using TestProf::FloatDuration
10
10
  include TestProf::Logging
11
11
 
12
- def dump(result, start_time:)
12
+ def dump(result, start_time:, threshold:)
13
13
  return log(:info, "No factories detected") if result.raw_stats == {}
14
14
  msgs = []
15
15
 
@@ -28,17 +28,34 @@ module TestProf::FactoryProf
28
28
  Total time: #{total_time.duration} (out of #{total_run_time.duration})
29
29
  Total uniq factories: #{total_uniq_factories}
30
30
 
31
- total top-level total time time per call top-level time name
31
+ name total top-level total time time per call top-level time
32
32
  MSG
33
33
 
34
34
  result.stats.each do |stat|
35
- time_per_call = stat[:total_time] / stat[:total_count]
36
-
37
- msgs << format("%8d %11d %13.4fs %17.4fs %18.4fs %18s", stat[:total_count], stat[:top_level_count], stat[:total_time], time_per_call, stat[:top_level_time], stat[:name])
35
+ next if stat[:total_count] < threshold
36
+
37
+ msgs << format("%-3s%-20s %8d %11d %13.4fs %17.4fs %18.4fs", *format_args(stat))
38
+ # move other variation ("[...]") to the end of the array
39
+ sorted_variations = stat[:variations].sort_by.with_index do |variation, i|
40
+ (variation[:name] == "[...]") ? stat[:variations].size + 1 : i
41
+ end
42
+ sorted_variations.each do |variation_stat|
43
+ msgs << format("%-5s%-18s %8d %11d %13.4fs %17.4fs %18.4fs", *format_args(variation_stat))
44
+ end
38
45
  end
39
46
 
40
47
  log :info, msgs.join("\n")
41
48
  end
49
+
50
+ private
51
+
52
+ def format_args(stat)
53
+ time_per_call = stat[:total_time] / stat[:total_count]
54
+ format_args = [""]
55
+ format_args += stat.values_at(:name, :total_count, :top_level_count, :total_time)
56
+ format_args << time_per_call
57
+ format_args << stat[:top_level_time]
58
+ end
42
59
  end
43
60
  end
44
61
  end
@@ -16,7 +16,7 @@ module TestProf
16
16
 
17
17
  # FactoryProf configuration
18
18
  class Configuration
19
- attr_accessor :mode, :printer
19
+ attr_accessor :mode, :printer, :threshold, :include_variations, :variations_limit
20
20
 
21
21
  def initialize
22
22
  @mode = (ENV["FPROF"] == "flamegraph") ? :flamegraph : :simple
@@ -31,6 +31,9 @@ module TestProf
31
31
  else
32
32
  Printers::Simple
33
33
  end
34
+ @threshold = ENV.fetch("FPROF_THRESHOLD", 0).to_i
35
+ @include_variations = ENV["FPROF_VARS"] == "1"
36
+ @variations_limit = ENV.fetch("FPROF_VARIATIONS_LIMIT", 2).to_i
34
37
  end
35
38
 
36
39
  # Whether we want to generate flamegraphs
@@ -49,8 +52,14 @@ module TestProf
49
52
 
50
53
  # Returns sorted stats
51
54
  def stats
52
- @stats ||= @raw_stats.values
53
- .sort_by { |el| -el[:total_count] }
55
+ @stats ||= @raw_stats.values.sort_by { |el| -el[:total_count] }.map do |stat|
56
+ unless stat[:variations].empty?
57
+ stat = stat.dup
58
+ stat[:variations] = stat[:variations].values.sort_by { |nested_el| -nested_el[:total_count] }
59
+ end
60
+
61
+ stat
62
+ end
54
63
  end
55
64
 
56
65
  def total_count
@@ -60,14 +69,6 @@ module TestProf
60
69
  def total_time
61
70
  @total_time ||= @raw_stats.values.sum { |v| v[:total_time] }
62
71
  end
63
-
64
- private
65
-
66
- def sorted_stats(key)
67
- @raw_stats.values
68
- .map { |el| [el[:name], el[key]] }
69
- .sort_by { |el| -el[1] }
70
- end
71
72
  end
72
73
 
73
74
  class << self
@@ -115,7 +116,7 @@ module TestProf
115
116
  def print(started_at)
116
117
  printer = config.printer
117
118
 
118
- printer.dump(result, start_time: started_at)
119
+ printer.dump(result, start_time: started_at, threshold: config.threshold)
119
120
  end
120
121
 
121
122
  def start
@@ -131,20 +132,19 @@ module TestProf
131
132
  Result.new(@stacks, @stats)
132
133
  end
133
134
 
134
- def track(factory)
135
+ def track(factory, variation:)
135
136
  return yield unless running?
136
137
  @depth += 1
137
138
  @current_stack << factory if config.flamegraph?
138
- @stats[factory][:total_count] += 1
139
- @stats[factory][:top_level_count] += 1 if @depth == 1
139
+ track_count(@stats[factory])
140
+ track_count(@stats[factory][:variations][variation_name(variation)]) unless variation.empty?
140
141
  t1 = TestProf.now
141
142
  begin
142
143
  yield
143
144
  ensure
144
145
  t2 = TestProf.now
145
- elapsed = t2 - t1
146
- @stats[factory][:total_time] += elapsed
147
- @stats[factory][:top_level_time] += elapsed if @depth == 1
146
+ track_time(@stats[factory], t1, t2)
147
+ track_time(@stats[factory][:variations][variation_name(variation)], t1, t2) unless variation.empty?
148
148
  @depth -= 1
149
149
  flush_stack if @depth.zero?
150
150
  end
@@ -152,21 +152,45 @@ module TestProf
152
152
 
153
153
  private
154
154
 
155
+ def variation_name(variation)
156
+ variations_count = variation.to_s.scan(/[\w]+/).size
157
+ return "[...]" if variations_count > config.variations_limit
158
+
159
+ variation
160
+ end
161
+
155
162
  def reset!
156
163
  @stacks = [] if config.flamegraph?
157
164
  @depth = 0
158
165
  @stats = Hash.new do |h, k|
159
- h[k] = {
160
- name: k,
161
- total_count: 0,
162
- top_level_count: 0,
163
- total_time: 0.0,
164
- top_level_time: 0.0
165
- }
166
+ h[k] = hash_template(k)
167
+ h[k][:variations] = Hash.new { |hh, variation_key| hh[variation_key] = hash_template(variation_key) }
168
+ h[k]
166
169
  end
167
170
  flush_stack
168
171
  end
169
172
 
173
+ def hash_template(name)
174
+ {
175
+ name: name,
176
+ total_count: 0,
177
+ top_level_count: 0,
178
+ total_time: 0.0,
179
+ top_level_time: 0.0
180
+ }
181
+ end
182
+
183
+ def track_count(factory)
184
+ factory[:total_count] += 1
185
+ factory[:top_level_count] += 1 if @depth == 1
186
+ end
187
+
188
+ def track_time(factory, t1, t2)
189
+ elapsed = t2 - t1
190
+ factory[:total_time] += elapsed
191
+ factory[:top_level_time] += elapsed if @depth == 1
192
+ end
193
+
170
194
  def flush_stack
171
195
  return unless config.flamegraph?
172
196
  @stacks << @current_stack unless @current_stack.nil? || @current_stack.empty?
@@ -7,14 +7,16 @@ module TestProf
7
7
  CORE_RUNNABLES = [
8
8
  Minitest::Test,
9
9
  defined?(Minitest::Unit::TestCase) ? Minitest::Unit::TestCase : nil,
10
- Minitest::Spec
10
+ defined?(Minitest::Spec) ? Minitest::Spec : nil
11
11
  ].compact.freeze
12
12
 
13
13
  class << self
14
14
  def suites
15
15
  # Make sure that sample contains only _real_ suites
16
16
  Minitest::Runnable.runnables
17
- .reject { |suite| CORE_RUNNABLES.include?(suite) }
17
+ .select do |suite|
18
+ CORE_RUNNABLES.any? { |kl| suite < kl } && suite.runnable_methods.any?
19
+ end
18
20
  end
19
21
 
20
22
  def sample_groups(sample_size)
@@ -27,11 +29,18 @@ module TestProf
27
29
  all_examples = suites.flat_map do |runnable|
28
30
  runnable.runnable_methods.map { |method| [runnable, method] }
29
31
  end
30
- sample = all_examples.sample(sample_size)
32
+
33
+ sample = all_examples.sample(sample_size).group_by(&:first)
34
+ sample.transform_values! { |v| v.map(&:last) }
35
+
31
36
  # Filter examples by overriding #runnable_methods for all suites
32
37
  suites.each do |runnable|
33
- runnable.define_singleton_method(:runnable_methods) do
34
- super() & sample.select { |ex| ex.first.equal?(runnable) }.map(&:last)
38
+ if sample.key?(runnable)
39
+ runnable.define_singleton_method(:runnable_methods) do
40
+ super() & sample[runnable]
41
+ end
42
+ else
43
+ runnable.define_singleton_method(:runnable_methods) { [] }
35
44
  end
36
45
  end
37
46
  end
@@ -7,6 +7,8 @@ module TestProf
7
7
  # Just like `let`, but persist the result for the whole group.
8
8
  # NOTE: Experimental and magical, for more control use `before_all`.
9
9
  module LetItBe
10
+ class DuplicationError < StandardError; end
11
+
10
12
  Modifier = Struct.new(:scope, :block) do
11
13
  def call(record, config)
12
14
  block.call(record, config)
@@ -14,6 +16,8 @@ module TestProf
14
16
  end
15
17
 
16
18
  class Configuration
19
+ attr_accessor :report_duplicates
20
+
17
21
  # Define an alias for `let_it_be` with the predefined options:
18
22
  #
19
23
  # TestProf::LetItBe.configure do |config|
@@ -117,6 +121,8 @@ module TestProf
117
121
  instance_variable_get(:"#{PREFIX}#{identifier}")
118
122
  end
119
123
 
124
+ report_duplicates(identifier) if LetItBe.config.report_duplicates
125
+
120
126
  LetItBe.module_for(self).module_eval do
121
127
  define_method(identifier) do
122
128
  # Trying to detect the context
@@ -135,6 +141,19 @@ module TestProf
135
141
  let(identifier, &let_accessor)
136
142
  end
137
143
 
144
+ private def report_duplicates(identifier)
145
+ if instance_methods.include?(identifier) && File.basename(__FILE__) == File.basename(instance_method(identifier).source_location[0])
146
+ error_msg = "let_it_be(:#{identifier}) was redefined in nested group"
147
+ report_level = LetItBe.config.report_duplicates.to_sym
148
+
149
+ if report_level == :warn
150
+ ::RSpec.warn_with(error_msg)
151
+ elsif report_level == :raise
152
+ raise DuplicationError, error_msg
153
+ end
154
+ end
155
+ end
156
+
138
157
  module Freezer
139
158
  # Stoplist to prevent freezing objects and theirs associations that are defined
140
159
  # with `let_it_be`'s `freeze: false` options during deep freezing.
@@ -19,7 +19,7 @@ module TestProf
19
19
  module Vernier
20
20
  # Vernier configuration
21
21
  class Configuration
22
- attr_accessor :mode, :target, :interval
22
+ attr_accessor :mode, :target, :interval, :hooks
23
23
 
24
24
  def initialize
25
25
  @mode = ENV.fetch("TEST_VERNIER_MODE", :wall).to_sym
@@ -27,6 +27,7 @@ module TestProf
27
27
 
28
28
  sample_interval = ENV["TEST_VERNIER_INTERVAL"].to_i
29
29
  @interval = (sample_interval > 0) ? sample_interval : nil
30
+ @hooks = ENV["TEST_VERNIER_HOOKS"]&.split(",")&.map { |hook| hook.strip.to_sym }
30
31
  end
31
32
 
32
33
  def boot?
@@ -82,6 +83,7 @@ module TestProf
82
83
  options = {}
83
84
 
84
85
  options[:interval] = config.interval if config.interval
86
+ options[:hooks] = config.hooks if config.hooks
85
87
 
86
88
  if block_given?
87
89
  options[:mode] = config.mode
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "1.3.3.1"
4
+ VERSION = "1.4.0.rc.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-prof
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.3.1
4
+ version: 1.4.0.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-29 00:00:00.000000000 Z
11
+ date: 2024-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -243,12 +243,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
243
243
  requirements:
244
244
  - - ">="
245
245
  - !ruby/object:Gem::Version
246
- version: 2.5.0
246
+ version: 2.7.0
247
247
  required_rubygems_version: !ruby/object:Gem::Requirement
248
248
  requirements:
249
- - - ">="
249
+ - - ">"
250
250
  - !ruby/object:Gem::Version
251
- version: '0'
251
+ version: 1.3.1
252
252
  requirements: []
253
253
  rubygems_version: 3.4.19
254
254
  signing_key: