test-prof 1.0.10 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/lib/test_prof/before_all/adapters/active_record.rb +1 -1
- data/lib/test_prof/before_all.rb +44 -17
- data/lib/test_prof/event_prof/monitor.rb +10 -3
- data/lib/test_prof/event_prof.rb +1 -1
- data/lib/test_prof/factory_default/factory_bot_patch.rb +3 -8
- data/lib/test_prof/factory_default.rb +330 -16
- data/lib/test_prof/factory_doctor/minitest.rb +1 -1
- data/lib/test_prof/factory_prof.rb +1 -1
- data/lib/test_prof/recipes/rspec/before_all.rb +5 -4
- data/lib/test_prof/recipes/rspec/factory_default.rb +20 -1
- data/lib/test_prof/recipes/rspec/let_it_be.rb +21 -9
- data/lib/test_prof/rspec_dissect.rb +1 -1
- data/lib/test_prof/stack_prof.rb +5 -4
- data/lib/test_prof/tag_prof/rspec.rb +1 -1
- data/lib/test_prof/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e19d20bbf0af4e016f9dabfe56e8074cce79d750a359555a2855b982298aa8b
|
4
|
+
data.tar.gz: 609607e479fabd363a6ee27e20b2f55845c9a5682fdec6352b5411b7c20e5ed5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/test_prof/before_all.rb
CHANGED
@@ -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(
|
88
|
+
def run(scope = nil, metadata = [])
|
89
|
+
before.each { |hook| hook.run(scope, metadata) }
|
63
90
|
yield
|
64
|
-
after.each(
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
data/lib/test_prof/event_prof.rb
CHANGED
@@ -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::
|
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
|
-
|
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) ||
|
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
|
-
|
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
|
-
|
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:
|
21
|
-
FactoryDefault.register(
|
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
|
-
|
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
|
-
|
36
|
-
@
|
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
|
-
|
41
|
-
|
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 &&
|
50
|
-
|
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[:
|
380
|
+
Thread.current[:testprof_factory_default_store] ||= {}
|
67
381
|
end
|
68
382
|
end
|
69
383
|
end
|
@@ -17,23 +17,24 @@ module TestProf
|
|
17
17
|
return
|
18
18
|
end
|
19
19
|
|
20
|
-
@
|
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?(:@
|
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
|
-
|
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
|
-
|
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?(:
|
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?(:
|
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};" \
|
data/lib/test_prof/stack_prof.rb
CHANGED
@@ -24,11 +24,10 @@ module TestProf
|
|
24
24
|
class Configuration
|
25
25
|
FORMATS = %w[html json].freeze
|
26
26
|
|
27
|
-
attr_accessor :mode, :
|
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?
|
data/lib/test_prof/version.rb
CHANGED
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
|
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-
|
11
|
+
date: 2022-12-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|