test-prof 1.3.3.1 → 1.4.0.rc.1

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