test-prof 1.0.10 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d847a0016b2749e8ba8a637d2db3d0044f7b77cfc5962596ff490aea0f5fc7d
4
- data.tar.gz: 1f32af0e6e09b555bf258c5a52586a9d4a768578b0c816298343dfd5deb826da
3
+ metadata.gz: 9e19d20bbf0af4e016f9dabfe56e8074cce79d750a359555a2855b982298aa8b
4
+ data.tar.gz: 609607e479fabd363a6ee27e20b2f55845c9a5682fdec6352b5411b7c20e5ed5
5
5
  SHA512:
6
- metadata.gz: 55db394aef01457404c8f5cd914cfa0eb359f3bea08874c6d4a6b75b1151af5b4153087815c90ed73ea5948c403941918b75af0b79558afe795db77c4d2b2568
7
- data.tar.gz: d95cfbd7c513d086cd47b2ce0a804ec52fdf174854c0a07c239289c65d97d18833529321e0ffb4853aabc802f05a8a8eac4e3fcdc7011f0ffd1bf0c60d150b41
6
+ metadata.gz: a17fe83206dd28c8b632acc3cfeb79c1acdf8344fbb199f1a688f1e61ef9f11097f0de0cc04ba1d153d108c2e966622062ea4e9917c2e8083c921de7b5bba9b6
7
+ data.tar.gz: cdaf249b8072a916dad83d690d32d9264ff40c46c542f26374fd217324af99a04be61560753d37ee44a0298f05b71d1ef0616e62bf020035414c4ed28d51fab3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 1.1.0 (2022-12-06)
6
+
7
+ - LetItBe: freeze records during initialization with `freeze: true`. ([@palkan][])
8
+
9
+ - Add FactoryDefault profiler (factory associations profilers). ([@palkan][])
10
+
11
+ - FactoryDefault: Allow creating a default per trait (or set of traits). ([@palkan][])
12
+
13
+ Now `create_default(:user)` and `create_default(:user, :admin)` would result into two defaults corresponding to the specified traits.
14
+
15
+ - FactoryDefault: Add stats support. ([@palkan][])
16
+
17
+ Now you can see how often the default factory values have been used by specifying
18
+ the `FACTORY_DEFAULT_SUMMARY=1` or `FACTORY_DEFAULT_STATS=1` env var.
19
+
20
+ - Support using FactoryDefault with before_all/let_it_be. ([@palkan][])
21
+
22
+ Currently, RSpec only. Default factories created within `before_all` or `let_it_be` are not reset 'till the end of the corresponding context. Thus, now it's possible to use `create_default` within `let_it_be` without any additional hacks.
23
+
24
+ - FactoryDefault: Add `preserve_attributes = false | true` option. ([@palkan][])
25
+
26
+ Allow skipping defaults if association is defined with overrides, e.g.:
27
+
28
+ ```ruby
29
+ factory :post do
30
+ association :user, name: "Post Author"
31
+ end
32
+ ```
33
+
34
+ - FactoryDefault: Add `skip_factory_default(&block)` to temporary disable default factories. ([@palkan][])
35
+
36
+ You can also use `TestProf::FactoryDefault.disable!(&block)`.
37
+
38
+ - Add support for global `before_all` tags ([@maxshend][])
39
+
40
+ ```ruby
41
+ TestProf::BeforeAll.configure do |config|
42
+ config.before(:begin, reset_sequences: true, foo: :bar) do
43
+ warn <<~MESSAGE
44
+ Do NOT create objects outside of transaction
45
+ because all db sequences will be reset to 1
46
+ in every single example, so that IDs of new objects
47
+ can get into conflict with the long-living ones.
48
+ MESSAGE
49
+ end
50
+ end
51
+ ```
52
+
53
+ ## 1.0.11 (2022-10-27)
54
+
55
+ - Fix monitoring methods with keyword args in Ruby 3+. ([@palkan][])
56
+
57
+ - Disable garbage collection frames when `TEST_STACK_PROF_IGNORE_GC` env variable is set ([@cbliard][])
58
+
59
+ - Fixed restoring lock_thread value in nested contexts ([@ygelfand][])
60
+
5
61
  ## 1.0.10 (2022-08-12)
6
62
 
7
63
  - Allow overriding global logger. ([@palkan][])
@@ -294,3 +350,6 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
294
350
  [@grillermo]: https://github.com/grillermo
295
351
  [@cou929]: https://github.com/cou929
296
352
  [@ruslanshakirov]: https://github.com/ruslanshakirov
353
+ [@ygelfand]: https://github.com/ygelfand
354
+ [@cbliard]: https://github.com/cbliard
355
+ [@maxshend]: https://github.com/maxshend
@@ -47,7 +47,7 @@ module TestProf
47
47
  # might lead to leaking connections
48
48
  config.before(:begin) do
49
49
  next unless ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
50
- instance_variable_set("#{PREFIX_RESTORE_LOCK_THREAD}_orig_lock_thread", ::ActiveRecord::Base.connection.pool.instance_variable_get(:@lock_thread))
50
+ 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"
51
51
  ::ActiveRecord::Base.connection.pool.lock_thread = true
52
52
  end
53
53
 
@@ -17,19 +17,19 @@ module TestProf
17
17
  class << self
18
18
  attr_accessor :adapter
19
19
 
20
- def begin_transaction
20
+ def begin_transaction(scope = nil, metadata = [])
21
21
  raise AdapterMissing if adapter.nil?
22
22
 
23
- config.run_hooks(:begin) do
23
+ config.run_hooks(:begin, scope, metadata) do
24
24
  adapter.begin_transaction
25
25
  end
26
26
  yield
27
27
  end
28
28
 
29
- def rollback_transaction
29
+ def rollback_transaction(scope = nil, metadata = [])
30
30
  raise AdapterMissing if adapter.nil?
31
31
 
32
- config.run_hooks(:rollback) do
32
+ config.run_hooks(:rollback, scope, metadata) do
33
33
  adapter.rollback_transaction
34
34
  end
35
35
  end
@@ -49,6 +49,33 @@ module TestProf
49
49
  end
50
50
  end
51
51
 
52
+ class HookEntry # :nodoc:
53
+ attr_reader :filters, :block
54
+
55
+ def initialize(block:, filters: [])
56
+ @block = block
57
+ @filters = TestProf.rspec? ? ::RSpec::Core::Metadata.build_hash_from(filters) : filters
58
+ end
59
+
60
+ def run(scope, metadata)
61
+ return unless filters_apply?(metadata)
62
+
63
+ block.call(scope)
64
+ end
65
+
66
+ private
67
+
68
+ def filters_apply?(metadata)
69
+ return true unless filters.present? && TestProf.rspec?
70
+
71
+ ::RSpec::Core::MetadataFilter.apply?(
72
+ :all?,
73
+ filters,
74
+ metadata
75
+ )
76
+ end
77
+ end
78
+
52
79
  class HooksChain # :nodoc:
53
80
  attr_reader :type, :after, :before
54
81
 
@@ -58,10 +85,10 @@ module TestProf
58
85
  @after = []
59
86
  end
60
87
 
61
- def run
62
- before.each(&:call)
88
+ def run(scope = nil, metadata = [])
89
+ before.each { |hook| hook.run(scope, metadata) }
63
90
  yield
64
- after.each(&:call)
91
+ after.each { |hook| hook.run(scope, metadata) }
65
92
  end
66
93
  end
67
94
 
@@ -76,26 +103,26 @@ module TestProf
76
103
  end
77
104
 
78
105
  # Add `before` hook for `begin` or
79
- # `rollback` operation:
106
+ # `rollback` operation with optional filters:
80
107
  #
81
- # config.before(:rollback) { ... }
82
- def before(type, &block)
108
+ # config.before(:rollback, foo: :bar) { ... }
109
+ def before(type, *filters, &block)
83
110
  validate_hook_type!(type)
84
- hooks[type].before << block if block
111
+ hooks[type].before << HookEntry.new(block: block, filters: filters) if block
85
112
  end
86
113
 
87
114
  # Add `after` hook for `begin` or
88
- # `rollback` operation:
115
+ # `rollback` operation with optional filters:
89
116
  #
90
- # config.after(:begin) { ... }
91
- def after(type, &block)
117
+ # config.after(:begin, foo: :bar) { ... }
118
+ def after(type, *filters, &block)
92
119
  validate_hook_type!(type)
93
- hooks[type].after << block if block
120
+ hooks[type].after << HookEntry.new(block: block, filters: filters) if block
94
121
  end
95
122
 
96
- def run_hooks(type) # :nodoc:
123
+ def run_hooks(type, scope = nil, metadata = []) # :nodoc:
97
124
  validate_hook_type!(type)
98
- hooks[type].run { yield }
125
+ hooks[type].run(scope, metadata) { yield }
99
126
  end
100
127
 
101
128
  private
@@ -48,9 +48,16 @@ module TestProf
48
48
 
49
49
  patch = Module.new do
50
50
  mids.each do |mid|
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) }
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
54
61
  end
55
62
  end
56
63
  end
@@ -8,7 +8,7 @@ require "test_prof/utils/sized_ordered_set"
8
8
 
9
9
  module TestProf
10
10
  # EventProf profiles your tests and suites against custom events,
11
- # such as ActiveSupport::Notifacations.
11
+ # such as ActiveSupport::Notifications.
12
12
  #
13
13
  # It works very similar to `rspec --profile` but can track arbitrary events.
14
14
  #
@@ -4,13 +4,7 @@ module TestProf
4
4
  module FactoryDefault # :nodoc: all
5
5
  module RunnerExt
6
6
  refine TestProf::FactoryBot::FactoryRunner do
7
- def name
8
- @name
9
- end
10
-
11
- def traits
12
- @traits
13
- end
7
+ attr_reader :name, :traits, :overrides
14
8
  end
15
9
  end
16
10
 
@@ -18,7 +12,8 @@ module TestProf
18
12
 
19
13
  module StrategyExt
20
14
  def association(runner)
21
- FactoryDefault.get(runner.name, runner.traits) || super
15
+ FactoryDefault.get(runner.name, runner.traits, runner.overrides) ||
16
+ FactoryDefault.profiler.instrument(runner.name, runner.traits, runner.overrides) { super }
22
17
  end
23
18
  end
24
19
  end
@@ -3,27 +3,198 @@
3
3
  require "test_prof"
4
4
  require "test_prof/factory_bot"
5
5
  require "test_prof/factory_default/factory_bot_patch"
6
+ require "test_prof/ext/float_duration"
7
+ require "test_prof/ext/active_record_refind" if defined?(::ActiveRecord::Base)
6
8
 
7
9
  module TestProf
8
10
  # FactoryDefault allows use to re-use associated objects
9
11
  # in factories implicilty
10
12
  module FactoryDefault
13
+ using FloatDuration
14
+ using Ext::ActiveRecordRefind if defined?(::ActiveRecord::Base)
15
+
16
+ using(Module.new do
17
+ refine Object do
18
+ def to_override_key
19
+ "<#{self.class.name}::$id$#{object_id}$di$>"
20
+ end
21
+
22
+ def refind
23
+ self
24
+ end
25
+ end
26
+
27
+ if defined?(::ActiveRecord::Base)
28
+ refine ::ActiveRecord::Base do
29
+ def to_override_key
30
+ "<#{self.class.name}\#$id$#{public_send(self.class.primary_key)}$di$>"
31
+ end
32
+ end
33
+ end
34
+
35
+ [
36
+ String,
37
+ Integer,
38
+ Float,
39
+ FalseClass,
40
+ TrueClass,
41
+ NilClass,
42
+ Regexp
43
+ ].each do |mod|
44
+ refine(mod) do
45
+ def to_override_key
46
+ inspect
47
+ end
48
+ end
49
+ end
50
+ end)
51
+
52
+ class Profiler
53
+ include Logging
54
+
55
+ attr_reader :data
56
+
57
+ def initialize
58
+ @data = Hash.new { |h, k| h[k] = {count: 0, time: 0.0} }
59
+ end
60
+
61
+ def instrument(name, traits, overrides)
62
+ start = TestProf.now
63
+ yield.tap do
64
+ time = TestProf.now - start
65
+ key = build_association_name(name, traits, overrides)
66
+ data[key][:count] += 1
67
+ data[key][:time] += time
68
+ end
69
+ end
70
+
71
+ def print_report
72
+ if data.empty?
73
+ log :info, "FactoryDefault profiler collected no data"
74
+ return
75
+ end
76
+
77
+ # Merge object overrides into one stats record
78
+ data = self.data.each_with_object({}) do |(name, stats), acc|
79
+ name = name.gsub(/\$id\$.+\$di\$/, "<id>")
80
+ if acc.key?(name)
81
+ acc[name][:count] += stats[:count]
82
+ acc[name][:time] += stats[:time]
83
+ else
84
+ acc[name] = stats
85
+ end
86
+ end
87
+
88
+ msgs = []
89
+
90
+ msgs <<
91
+ <<~MSG
92
+ Factory associations usage:
93
+ MSG
94
+
95
+ first_column = data.keys.map(&:size).max + 2
96
+
97
+ msgs << format(
98
+ "%#{first_column}s %9s %12s",
99
+ "factory", "count", "total time"
100
+ )
101
+
102
+ msgs << ""
103
+
104
+ total_count = 0
105
+ total_time = 0.0
106
+
107
+ data.to_a.sort_by { |(_, v)| -v[:time] }.each do |(key, factory_stats)|
108
+ total_count += factory_stats[:count]
109
+ total_time += factory_stats[:time]
110
+
111
+ msgs << format(
112
+ "%#{first_column}s %9d %12s",
113
+ key, factory_stats[:count], factory_stats[:time].duration
114
+ )
115
+ end
116
+
117
+ msgs <<
118
+ <<~MSG
119
+
120
+ Total associations created: #{total_count}
121
+ Total uniq associations created: #{data.size}
122
+ Total time spent: #{total_time.duration}
123
+
124
+ MSG
125
+
126
+ log :info, msgs.join("\n")
127
+ end
128
+
129
+ private
130
+
131
+ def build_association_name(name, traits, overrides)
132
+ traits_str = "[#{traits.join(",")}]" if traits&.any?
133
+ overrides_str = "{#{overrides.map { |k, v| "#{k}:#{v.to_override_key}" }.join(",")}}" if overrides&.any?
134
+ "#{name}#{traits_str}#{overrides_str}"
135
+ end
136
+ end
137
+
138
+ class NoopProfiler
139
+ def instrument(*)
140
+ yield
141
+ end
142
+
143
+ def print_report
144
+ end
145
+ end
146
+
11
147
  module DefaultSyntax # :nodoc:
12
148
  def create_default(name, *args, &block)
13
149
  options = args.extract_options!
14
- preserve = options.delete(:preserve_traits)
150
+ default_options = {}
151
+ default_options[:preserve_traits] = options.delete(:preserve_traits) if options.key?(:preserve_traits)
152
+ default_options[:preserve_attributes] = options.delete(:preserve_attributes) if options.key?(:preserve_attributes)
15
153
 
16
154
  obj = TestProf::FactoryBot.create(name, *args, options, &block)
17
- set_factory_default(name, obj, preserve_traits: preserve)
155
+
156
+ # Factory with traits
157
+ name = [name, *args] if args.any?
158
+
159
+ set_factory_default(name, obj, **default_options)
18
160
  end
19
161
 
20
- def set_factory_default(name, obj, preserve_traits: nil)
21
- FactoryDefault.register(name, obj, preserve_traits: preserve_traits)
162
+ def set_factory_default(name, obj, preserve_traits: FactoryDefault.config.preserve_traits, preserve_attributes: FactoryDefault.config.preserve_attributes, **other)
163
+ FactoryDefault.register(
164
+ name, obj,
165
+ preserve_traits: preserve_traits,
166
+ preserve_attributes: preserve_attributes,
167
+ **other
168
+ )
169
+ end
170
+
171
+ def skip_factory_default(&block)
172
+ FactoryDefault.disable!(&block)
173
+ end
174
+ end
175
+
176
+ class Configuration
177
+ attr_accessor :preserve_traits, :preserve_attributes,
178
+ :report_summary, :report_stats,
179
+ :profiling_enabled
180
+
181
+ alias_method :profiling_enabled?, :profiling_enabled
182
+
183
+ def initialize
184
+ # TODO(v2): Switch to true
185
+ @preserve_traits = false
186
+ @preserve_attributes = false
187
+ @profiling_enabled = ENV["FACTORY_DEFAULT_PROF"] == "1"
188
+ @report_summary = ENV["FACTORY_DEFAULT_SUMMARY"] == "1"
189
+ @report_stats = ENV["FACTORY_DEFAULT_STATS"] == "1"
22
190
  end
23
191
  end
24
192
 
25
193
  class << self
26
- attr_accessor :preserve_traits
194
+ include Logging
195
+
196
+ attr_accessor :current_context
197
+ attr_reader :stats, :profiler
27
198
 
28
199
  def init
29
200
  TestProf::FactoryBot::Syntax::Methods.include DefaultSyntax
@@ -32,38 +203,181 @@ module TestProf
32
203
  TestProf::FactoryBot::Strategy::Build.prepend StrategyExt
33
204
  TestProf::FactoryBot::Strategy::Stub.prepend StrategyExt
34
205
 
35
- # default is false to retain backward compatibility
36
- @preserve_traits = false
206
+ @profiler = config.profiling_enabled? ? Profiler.new : NoopProfiler.new
207
+ @enabled = ENV["FACTORY_DEFAULT_DISABLED"] != "1"
208
+ @stats = {}
209
+ end
210
+
211
+ def config
212
+ @config ||= Configuration.new
213
+ end
214
+
215
+ def configure
216
+ yield config
217
+ end
218
+
219
+ # TODO(v2): drop
220
+ def preserve_traits=(val)
221
+ config.preserve_traits = val
222
+ end
223
+
224
+ def preserve_attributes=(val)
225
+ config.preserve_attributes = val
37
226
  end
38
227
 
39
228
  def register(name, obj, **options)
40
- options[:preserve_traits] = true if FactoryDefault.preserve_traits
41
- store[name] = {object: obj, **options}
229
+ # Name with traits
230
+ if name.is_a?(Array)
231
+ register_traited_record(*name, obj, **options)
232
+ else
233
+ register_default_record(name, obj, **options)
234
+ end
235
+
42
236
  obj
43
237
  end
44
238
 
45
- def get(name, traits = nil)
239
+ def get(name, traits = nil, overrides = nil)
240
+ return unless enabled?
241
+
46
242
  record = store[name]
47
243
  return unless record
48
244
 
49
- if traits && !traits.empty?
50
- return if FactoryDefault.preserve_traits || record[:preserve_traits]
245
+ if traits && (trait_key = record[:traits][traits])
246
+ name = trait_key
247
+ record = store[name]
248
+ traits = nil
249
+ end
250
+
251
+ stats[name][:miss] += 1
252
+
253
+ if traits && !traits.empty? && record[:preserve_traits]
254
+ return
255
+ end
256
+
257
+ object = record[:object]
258
+
259
+ if overrides && !overrides.empty? && record[:preserve_attributes]
260
+ overrides.each do |name, value|
261
+ return unless object.respond_to?(name) # rubocop:disable Lint/NonLocalExitFromIterator
262
+ return if object.public_send(name) != value # rubocop:disable Lint/NonLocalExitFromIterator
263
+ end
264
+ end
265
+
266
+ stats[name][:miss] -= 1
267
+ stats[name][:hit] += 1
268
+
269
+ if record[:context] && (record[:context] != :example)
270
+ object.refind
271
+ else
272
+ object
51
273
  end
52
- record[:object]
53
274
  end
54
275
 
55
276
  def remove(name)
56
277
  store.delete(name)
57
278
  end
58
279
 
59
- def reset
60
- store.clear
280
+ def reset(context: nil)
281
+ return store.clear unless context
282
+
283
+ store.delete_if do |_name, metadata|
284
+ metadata[:context] == context
285
+ end
286
+ end
287
+
288
+ def enabled?
289
+ @enabled
290
+ end
291
+
292
+ def enable!
293
+ was_enabled = @enabled
294
+ @enabled = true
295
+ return unless block_given?
296
+ yield
297
+ ensure
298
+ @enabled = was_enabled
299
+ end
300
+
301
+ def disable!
302
+ was_enabled = @enabled
303
+ @enabled = false
304
+ return unless block_given?
305
+ yield
306
+ ensure
307
+ @enabled = was_enabled
308
+ end
309
+
310
+ def print_report
311
+ profiler.print_report
312
+ return unless config.report_stats || config.report_summary
313
+
314
+ if stats.empty?
315
+ log :info, "FactoryDefault has not been used"
316
+ return
317
+ end
318
+
319
+ msgs = []
320
+
321
+ if config.report_stats
322
+ msgs <<
323
+ <<~MSG
324
+ FactoryDefault usage stats:
325
+ MSG
326
+
327
+ first_column = stats.keys.map(&:size).max + 2
328
+
329
+ msgs << format(
330
+ "%#{first_column}s %9s %9s",
331
+ "factory", "hit", "miss"
332
+ )
333
+
334
+ msgs << ""
335
+ end
336
+
337
+ total_hit = 0
338
+ total_miss = 0
339
+
340
+ stats.to_a.sort_by { |(_, v)| -v[:hit] }.each do |(key, record_stats)|
341
+ total_hit += record_stats[:hit]
342
+ total_miss += record_stats[:miss]
343
+
344
+ if config.report_stats
345
+ msgs << format(
346
+ "%#{first_column}s %9d %9d",
347
+ key, record_stats[:hit], record_stats[:miss]
348
+ )
349
+ end
350
+ end
351
+
352
+ msgs << "" if config.report_stats
353
+
354
+ msgs <<
355
+ <<~MSG
356
+ FactoryDefault summary: hit=#{total_hit} miss=#{total_miss}
357
+ MSG
358
+
359
+ log :info, msgs.join("\n")
61
360
  end
62
361
 
63
362
  private
64
363
 
364
+ def register_default_record(name, obj, **options)
365
+ store[name] = {object: obj, traits: {}, context: current_context, **options}
366
+ stats[name] ||= {hit: 0, miss: 0}
367
+ end
368
+
369
+ def register_traited_record(name, *traits, obj, **options)
370
+ name_with_traits = "#{name}[#{traits.join(",")}]"
371
+
372
+ register_default_record(name_with_traits, obj, **options)
373
+ register_default_record(name, obj, **options) unless store[name]
374
+
375
+ # Add reference to the traited default to the original default record
376
+ store[name][:traits][traits] = name_with_traits
377
+ end
378
+
65
379
  def store
66
- Thread.current[:testprof_factory_store] ||= {}
380
+ Thread.current[:testprof_factory_default_store] ||= {}
67
381
  end
68
382
  end
69
383
  end
@@ -87,7 +87,7 @@ module Minitest
87
87
  private
88
88
 
89
89
  def pluralize_records(count)
90
- count == 1 ? "1 record" : "#{count} records"
90
+ (count == 1) ? "1 record" : "#{count} records"
91
91
  end
92
92
  end
93
93
  end
@@ -18,7 +18,7 @@ module TestProf
18
18
  attr_accessor :mode, :printer
19
19
 
20
20
  def initialize
21
- @mode = ENV["FPROF"] == "flamegraph" ? :flamegraph : :simple
21
+ @mode = (ENV["FPROF"] == "flamegraph") ? :flamegraph : :simple
22
22
  @printer =
23
23
  case ENV["FPROF"]
24
24
  when "flamegraph"
@@ -17,23 +17,24 @@ module TestProf
17
17
  return
18
18
  end
19
19
 
20
- @__before_all_activated__ = true
20
+ @__before_all_activation__ = context = self
21
+ current_metadata = metadata
21
22
 
22
23
  before(:all) do
23
24
  @__inspect_output = "before_all hook"
24
25
  BeforeAll.setup_fixtures(self) if setup_fixtures
25
- BeforeAll.begin_transaction do
26
+ BeforeAll.begin_transaction(context, current_metadata) do
26
27
  instance_eval(&block)
27
28
  end
28
29
  end
29
30
 
30
31
  after(:all) do
31
- BeforeAll.rollback_transaction
32
+ BeforeAll.rollback_transaction(context, current_metadata)
32
33
  end
33
34
  end
34
35
 
35
36
  def within_before_all?
36
- instance_variable_defined?(:@__before_all_activated__)
37
+ instance_variable_defined?(:@__before_all_activation__)
37
38
  end
38
39
  end
39
40
  end
@@ -4,6 +4,25 @@ require "test_prof/factory_default"
4
4
 
5
5
  TestProf::FactoryDefault.init
6
6
 
7
+ if defined?(TestProf::BeforeAll)
8
+ TestProf::BeforeAll.configure do |config|
9
+ config.before(:begin) do |context|
10
+ TestProf::FactoryDefault.current_context = context
11
+ end
12
+
13
+ config.after(:rollback) do |context|
14
+ TestProf::FactoryDefault.reset(context: context)
15
+ end
16
+ end
17
+ end
18
+
7
19
  RSpec.configure do |config|
8
- config.after(:each) { TestProf::FactoryDefault.reset }
20
+ if defined?(TestProf::BeforeAll)
21
+ config.before(:each) { TestProf::FactoryDefault.current_context = :example }
22
+ config.after(:each) { TestProf::FactoryDefault.reset(context: :example) }
23
+ else
24
+ config.after(:each) { TestProf::FactoryDefault.reset }
25
+ end
26
+
27
+ config.after(:suite) { TestProf::FactoryDefault.print_report }
9
28
  end
@@ -7,6 +7,12 @@ 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
+ Modifier = Struct.new(:scope, :block) do
11
+ def call(record, config)
12
+ block.call(record, config)
13
+ end
14
+ end
15
+
10
16
  class Configuration
11
17
  # Define an alias for `let_it_be` with the predefined options:
12
18
  #
@@ -17,10 +23,13 @@ module TestProf
17
23
  LetItBe.define_let_it_be_alias(name, **default_args)
18
24
  end
19
25
 
20
- def register_modifier(key, &block)
26
+ # Register modifier by providing the name of key,
27
+ # optional scope (when to apply the modifier, on initialization (:initialize)
28
+ # or when accessed # via let (:let))
29
+ def register_modifier(key, on: :let, &block)
21
30
  raise ArgumentError, "Modifier #{key} is already defined for let_it_be" if LetItBe.modifiers.key?(key)
22
31
 
23
- LetItBe.modifiers[key] = block
32
+ LetItBe.modifiers[key] = Modifier.new(on, block)
24
33
  end
25
34
 
26
35
  def default_modifiers
@@ -41,17 +50,18 @@ module TestProf
41
50
  @modifiers ||= {}
42
51
  end
43
52
 
44
- def wrap_with_modifiers(mods, &block)
53
+ def wrap_with_modifiers(mods, on: :let, &block)
54
+ mods = mods.select { |k, val| LetItBe.modifiers.fetch(k).scope == on }
45
55
  return block if mods.empty?
46
56
 
47
57
  validate_modifiers! mods
48
58
 
49
- -> {
59
+ proc do
50
60
  record = instance_eval(&block)
51
61
  mods.inject(record) do |rec, (k, v)|
52
62
  LetItBe.modifiers.fetch(k).call(rec, v)
53
63
  end
54
- }
64
+ end
55
65
  end
56
66
 
57
67
  def module_for(group)
@@ -97,9 +107,11 @@ module TestProf
97
107
 
98
108
  options = default_options.merge(options)
99
109
 
110
+ initializer = LetItBe.wrap_with_modifiers(options, on: :initialize, &initializer)
111
+
100
112
  before_all(&initializer)
101
113
 
102
- let_accessor = LetItBe.wrap_with_modifiers(options) do
114
+ let_accessor = LetItBe.wrap_with_modifiers(options, on: :let) do
103
115
  instance_variable_get(:"#{PREFIX}#{identifier}")
104
116
  end
105
117
 
@@ -192,7 +204,7 @@ if defined?(::ActiveRecord::Base)
192
204
 
193
205
  next record.reload if record.is_a?(::ActiveRecord::Base)
194
206
 
195
- if record.respond_to?(:map)
207
+ if record.respond_to?(:to_ary)
196
208
  next record.map do |rec|
197
209
  rec.is_a?(::ActiveRecord::Base) ? rec.reload : rec
198
210
  end
@@ -205,7 +217,7 @@ if defined?(::ActiveRecord::Base)
205
217
 
206
218
  next record.refind if record.is_a?(::ActiveRecord::Base)
207
219
 
208
- if record.respond_to?(:map)
220
+ if record.respond_to?(:to_ary)
209
221
  next record.map do |rec|
210
222
  rec.is_a?(::ActiveRecord::Base) ? rec.refind : rec
211
223
  end
@@ -213,7 +225,7 @@ if defined?(::ActiveRecord::Base)
213
225
  record
214
226
  end
215
227
 
216
- config.register_modifier :freeze do |record, val|
228
+ config.register_modifier :freeze, on: :initialize do |record, val|
217
229
  if val == false
218
230
  TestProf::LetItBe::Freezer::Stoplist.stop!(record)
219
231
  next record
@@ -47,7 +47,7 @@ module TestProf
47
47
  @let_top_count = (ENV["RD_PROF_LET_TOP"] || 3).to_i
48
48
  @top_count = (ENV["RD_PROF_TOP"] || 5).to_i
49
49
  @stamp = ENV["RD_PROF_STAMP"]
50
- @mode = ENV["RD_PROF"] == "1" ? "all" : ENV["RD_PROF"]
50
+ @mode = (ENV["RD_PROF"] == "1") ? "all" : ENV["RD_PROF"]
51
51
 
52
52
  unless MODES.include?(mode)
53
53
  raise "Unknown RSpecDissect mode: #{mode};" \
@@ -24,11 +24,10 @@ module TestProf
24
24
  class Configuration
25
25
  FORMATS = %w[html json].freeze
26
26
 
27
- attr_accessor :mode, :interval, :raw, :target, :format
28
-
27
+ attr_accessor :mode, :raw, :target, :format, :interval, :ignore_gc
29
28
  def initialize
30
29
  @mode = ENV.fetch("TEST_STACK_PROF_MODE", :wall).to_sym
31
- @target = ENV["TEST_STACK_PROF"] == "boot" ? :boot : :suite
30
+ @target = (ENV["TEST_STACK_PROF"] == "boot") ? :boot : :suite
32
31
  @raw = ENV["TEST_STACK_PROF_RAW"] != "0"
33
32
  @format =
34
33
  if FORMATS.include?(ENV["TEST_STACK_PROF_FORMAT"])
@@ -38,7 +37,8 @@ module TestProf
38
37
  end
39
38
 
40
39
  sample_interval = ENV["TEST_STACK_PROF_INTERVAL"].to_i
41
- @interval = sample_interval > 0 ? sample_interval : nil
40
+ @interval = (sample_interval > 0) ? sample_interval : nil
41
+ @ignore_gc = !ENV["TEST_STACK_PROF_IGNORE_GC"].nil?
42
42
  end
43
43
 
44
44
  def raw?
@@ -96,6 +96,7 @@ module TestProf
96
96
  }
97
97
 
98
98
  options[:interval] = config.interval if config.interval
99
+ options[:ignore_gc] = true if config.ignore_gc
99
100
 
100
101
  if block_given?
101
102
  options[:out] = build_path(name)
@@ -13,7 +13,7 @@ module TestProf
13
13
  attr_reader :result, :printer
14
14
 
15
15
  def initialize
16
- @printer = ENV["TAG_PROF_FORMAT"] == "html" ? Printers::HTML : Printers::Simple
16
+ @printer = (ENV["TAG_PROF_FORMAT"] == "html") ? Printers::HTML : Printers::Simple
17
17
 
18
18
  @result =
19
19
  if ENV["TAG_PROF_EVENT"].nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "1.0.10"
4
+ VERSION = "1.1.0"
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.0.10
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-12 00:00:00.000000000 Z
11
+ date: 2022-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler