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