test-prof 1.3.2 → 1.4.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +2 -2
- data/lib/test_prof/before_all/adapters/active_record.rb +60 -46
- data/lib/test_prof/before_all.rb +1 -1
- 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/memory_prof/printer.rb +2 -0
- data/lib/test_prof/memory_prof/rspec.rb +7 -4
- data/lib/test_prof/memory_prof/tracker/linked_list.rb +8 -6
- data/lib/test_prof/memory_prof/tracker.rb +14 -10
- 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 +9 -9
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,44 @@
|
|
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
|
+
|
39
|
+
## 1.3.3 (2024-04-19)
|
40
|
+
|
41
|
+
- Fix MemProf bugs. ([@palkan][])
|
42
|
+
|
5
43
|
## 1.3.2 (2024-03-08) 🌷
|
6
44
|
|
7
45
|
- Add Minitest support for TagProf. ([@lioneldebauge][])
|
@@ -386,3 +424,5 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
|
|
386
424
|
[@Vankiru]: https://github.com/Vankiru
|
387
425
|
[@uzushino]: https://github.com/uzushino
|
388
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
|
-
}
|
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/before_all.rb
CHANGED
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?
|
@@ -16,23 +16,26 @@ module TestProf
|
|
16
16
|
@tracker = MemoryProf.tracker
|
17
17
|
@printer = MemoryProf.printer(tracker)
|
18
18
|
|
19
|
+
@current_group = nil
|
20
|
+
@current_example = nil
|
21
|
+
|
19
22
|
@tracker.start
|
20
23
|
end
|
21
24
|
|
22
25
|
def example_started(notification)
|
23
|
-
tracker.example_started(example(notification))
|
26
|
+
tracker.example_started(notification.example, example(notification))
|
24
27
|
end
|
25
28
|
|
26
29
|
def example_finished(notification)
|
27
|
-
tracker.example_finished(example
|
30
|
+
tracker.example_finished(notification.example)
|
28
31
|
end
|
29
32
|
|
30
33
|
def example_group_started(notification)
|
31
|
-
tracker.group_started(group(notification))
|
34
|
+
tracker.group_started(notification.group, group(notification))
|
32
35
|
end
|
33
36
|
|
34
37
|
def example_group_finished(notification)
|
35
|
-
tracker.group_finished(group
|
38
|
+
tracker.group_finished(notification.group)
|
36
39
|
end
|
37
40
|
|
38
41
|
def report
|
@@ -52,19 +52,20 @@ module TestProf
|
|
52
52
|
attr_reader :head
|
53
53
|
|
54
54
|
def initialize(memory_at_start)
|
55
|
-
add_node(:total, memory_at_start)
|
55
|
+
add_node(:total, :total, memory_at_start)
|
56
56
|
end
|
57
57
|
|
58
|
-
def add_node(item, memory_at_start)
|
58
|
+
def add_node(id, item, memory_at_start)
|
59
59
|
@head = LinkedListNode.new(
|
60
|
+
id: id,
|
60
61
|
item: item,
|
61
62
|
previous: head,
|
62
63
|
memory_at_start: memory_at_start
|
63
64
|
)
|
64
65
|
end
|
65
66
|
|
66
|
-
def remove_node(
|
67
|
-
return if head.
|
67
|
+
def remove_node(id, memory_at_finish)
|
68
|
+
return if head.id != id
|
68
69
|
head.finish(memory_at_finish)
|
69
70
|
|
70
71
|
current = head
|
@@ -75,9 +76,10 @@ module TestProf
|
|
75
76
|
end
|
76
77
|
|
77
78
|
class LinkedListNode
|
78
|
-
attr_reader :item, :previous, :memory_at_start, :memory_at_finish, :nested_memory
|
79
|
+
attr_reader :id, :item, :previous, :memory_at_start, :memory_at_finish, :nested_memory
|
79
80
|
|
80
|
-
def initialize(item:, memory_at_start:, previous:)
|
81
|
+
def initialize(id:, item:, memory_at_start:, previous:)
|
82
|
+
@id = id
|
81
83
|
@item = item
|
82
84
|
@previous = previous
|
83
85
|
|
@@ -36,22 +36,26 @@ module TestProf
|
|
36
36
|
@total_memory = node.total_memory
|
37
37
|
end
|
38
38
|
|
39
|
-
def example_started(example)
|
40
|
-
list.add_node(example, track)
|
39
|
+
def example_started(id, example = id)
|
40
|
+
list.add_node(id, example, track)
|
41
41
|
end
|
42
42
|
|
43
|
-
def example_finished(
|
44
|
-
node = list.remove_node(
|
45
|
-
|
43
|
+
def example_finished(id)
|
44
|
+
node = list.remove_node(id, track)
|
45
|
+
return unless node
|
46
|
+
|
47
|
+
examples << {**node.item, memory: node.total_memory}
|
46
48
|
end
|
47
49
|
|
48
|
-
def group_started(group)
|
49
|
-
list.add_node(group, track)
|
50
|
+
def group_started(id, group = id)
|
51
|
+
list.add_node(id, group, track)
|
50
52
|
end
|
51
53
|
|
52
|
-
def group_finished(
|
53
|
-
node = list.remove_node(
|
54
|
-
|
54
|
+
def group_finished(id)
|
55
|
+
node = list.remove_node(id, track)
|
56
|
+
return unless node
|
57
|
+
|
58
|
+
groups << {**node.item, memory: node.hooks_memory}
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
@@ -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
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -235,7 +235,7 @@ metadata:
|
|
235
235
|
homepage_uri: https://test-prof.evilmartians.io/
|
236
236
|
source_code_uri: https://github.com/test-prof/test-prof
|
237
237
|
funding_uri: https://github.com/sponsors/test-prof
|
238
|
-
post_install_message:
|
238
|
+
post_install_message:
|
239
239
|
rdoc_options: []
|
240
240
|
require_paths:
|
241
241
|
- lib
|
@@ -243,15 +243,15 @@ 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
|
-
rubygems_version: 3.4.
|
254
|
-
signing_key:
|
253
|
+
rubygems_version: 3.4.19
|
254
|
+
signing_key:
|
255
255
|
specification_version: 4
|
256
256
|
summary: Ruby applications tests profiling tools
|
257
257
|
test_files: []
|