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 +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +2 -2
- data/lib/test_prof/before_all/adapters/active_record.rb +60 -46
- data/lib/test_prof/core.rb +24 -1
- data/lib/test_prof/event_prof/monitor.rb +3 -10
- data/lib/test_prof/factory_bot.rb +2 -0
- data/lib/test_prof/factory_default/factory_bot_patch.rb +1 -1
- data/lib/test_prof/factory_default.rb +12 -5
- data/lib/test_prof/factory_prof/fabrication_patch.rb +7 -1
- data/lib/test_prof/factory_prof/factory_bot_patch.rb +15 -1
- data/lib/test_prof/factory_prof/factory_builders/fabrication.rb +2 -2
- data/lib/test_prof/factory_prof/factory_builders/factory_bot.rb +2 -2
- data/lib/test_prof/factory_prof/printers/json.rb +1 -1
- data/lib/test_prof/factory_prof/printers/nate_heckler.rb +1 -1
- data/lib/test_prof/factory_prof/printers/simple.rb +22 -5
- data/lib/test_prof/factory_prof.rb +49 -25
- data/lib/test_prof/recipes/minitest/sample.rb +14 -5
- data/lib/test_prof/recipes/rspec/let_it_be.rb +19 -0
- data/lib/test_prof/vernier.rb +3 -1
- data/lib/test_prof/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2bb23a2e0630d64b02f4f755f801189a19a5d26179dcb5d9d9420b73a0b78319
|
4
|
+
data.tar.gz: 56fe8596860ac93bb46adb6d6c9ff027de6eddf43eb3fbd28d69f2fddd76c1eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
-
|
12
|
-
|
13
|
-
::ActiveRecord::Base.connection_handler.connection_pool_list(
|
14
|
-
|
15
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
39
|
+
def pool_connection_errors
|
40
|
+
@pool_connection_errors ||= []
|
41
|
+
end
|
33
42
|
|
34
|
-
|
35
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
71
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
data/lib/test_prof/core.rb
CHANGED
@@ -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.
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
267
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
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
|
-
|
36
|
-
|
37
|
-
msgs << format("%8d %11d %13.4fs %17.4fs %18.4fs
|
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
|
-
|
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]
|
139
|
-
@stats[factory][:
|
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
|
-
|
146
|
-
@stats[factory][:
|
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
|
-
|
161
|
-
|
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
|
-
.
|
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
|
-
|
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
|
-
|
34
|
-
|
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.
|
data/lib/test_prof/vernier.rb
CHANGED
@@ -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
|
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.
|
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-
|
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.
|
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:
|
251
|
+
version: 1.3.1
|
252
252
|
requirements: []
|
253
253
|
rubygems_version: 3.4.19
|
254
254
|
signing_key:
|