test-prof 1.5.0 → 1.6.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 +24 -0
- data/lib/{test_prof → rubocop/test_prof}/cops/rspec/aggregate_examples/matchers_with_side_effects.rb +1 -1
- data/lib/{test_prof → rubocop/test_prof}/cops/rspec/aggregate_examples/node_matchers.rb +1 -1
- data/lib/{test_prof → rubocop/test_prof}/cops/rspec/aggregate_examples.rb +5 -5
- data/lib/{test_prof/cops → rubocop/test_prof}/plugin.rb +2 -2
- data/lib/rubocop/test_prof.rb +9 -0
- data/lib/test-prof.rb +5 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +1 -1
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +1 -1
- data/lib/test_prof/memory_prof/printer.rb +31 -0
- data/lib/test_prof/memory_prof/rspec.rb +2 -0
- data/lib/test_prof/memory_prof/tracker.rb +11 -1
- data/lib/test_prof/memory_prof.rb +8 -3
- data/lib/test_prof/recipes/rspec/let_it_be.rb +1 -1
- data/lib/test_prof/rspec_dissect/collector.rb +58 -0
- data/lib/test_prof/rspec_dissect/rspec.rb +13 -21
- data/lib/test_prof/rspec_dissect.rb +45 -51
- data/lib/test_prof/rspec_stamp/parser/prism.rb +75 -0
- data/lib/test_prof/rspec_stamp/parser/ripper.rb +128 -0
- data/lib/test_prof/rspec_stamp/parser.rb +8 -120
- data/lib/test_prof/rspec_stamp.rb +1 -0
- data/lib/test_prof/rubocop.rb +10 -9
- data/lib/test_prof/tps_prof/profiler.rb +52 -15
- data/lib/test_prof/tps_prof/reporter/text.rb +27 -7
- data/lib/test_prof/tps_prof/rspec.rb +25 -2
- data/lib/test_prof/tps_prof.rb +69 -4
- data/lib/test_prof/version.rb +1 -1
- metadata +14 -13
- data/lib/test_prof/rspec_dissect/collectors/base.rb +0 -70
- data/lib/test_prof/rspec_dissect/collectors/before.rb +0 -19
- data/lib/test_prof/rspec_dissect/collectors/let.rb +0 -39
- /data/lib/{test_prof → rubocop/test_prof}/cops/rspec/aggregate_examples/its.rb +0 -0
- /data/lib/{test_prof → rubocop/test_prof}/cops/rspec/aggregate_examples/line_range_helpers.rb +0 -0
- /data/lib/{test_prof → rubocop/test_prof}/cops/rspec/aggregate_examples/metadata_helpers.rb +0 -0
- /data/lib/{test_prof → rubocop/test_prof}/cops/rspec/language.rb +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
|
|
5
|
+
module TestProf
|
|
6
|
+
module RSpecStamp
|
|
7
|
+
module Parser
|
|
8
|
+
class Ripper
|
|
9
|
+
def parse(code)
|
|
10
|
+
sexp = ::Ripper.sexp(code)
|
|
11
|
+
return unless sexp
|
|
12
|
+
|
|
13
|
+
# sexp has the following format:
|
|
14
|
+
# [:program,
|
|
15
|
+
# [
|
|
16
|
+
# [
|
|
17
|
+
# :command,
|
|
18
|
+
# [:@ident, "it", [1, 0]],
|
|
19
|
+
# [:args_add_block, [ ... ]]
|
|
20
|
+
# ]
|
|
21
|
+
# ]
|
|
22
|
+
# ]
|
|
23
|
+
#
|
|
24
|
+
# or
|
|
25
|
+
#
|
|
26
|
+
# [:program,
|
|
27
|
+
# [
|
|
28
|
+
# [
|
|
29
|
+
# :vcall,
|
|
30
|
+
# [:@ident, "it", [1, 0]]
|
|
31
|
+
# ]
|
|
32
|
+
# ]
|
|
33
|
+
# ]
|
|
34
|
+
res = Result.new
|
|
35
|
+
|
|
36
|
+
fcall = sexp[1][0][1]
|
|
37
|
+
args_block = sexp[1][0][2]
|
|
38
|
+
|
|
39
|
+
if fcall.first == :fcall
|
|
40
|
+
fcall = fcall[1]
|
|
41
|
+
elsif fcall.first == :var_ref
|
|
42
|
+
res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
|
|
43
|
+
args_block = sexp[1][0][4]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
res.fname ||= fcall[1]
|
|
47
|
+
|
|
48
|
+
return res if args_block.nil?
|
|
49
|
+
|
|
50
|
+
args_block = args_block[1] if args_block.first == :arg_paren
|
|
51
|
+
|
|
52
|
+
args = args_block[1]
|
|
53
|
+
|
|
54
|
+
if args.first.first == :string_literal
|
|
55
|
+
res.desc = parse_literal(args.shift)
|
|
56
|
+
elsif args.first.first == :var_ref || args.first.first == :const_path_ref
|
|
57
|
+
res.desc_const = parse_const(args.shift)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
parse_arg(res, args.shift) until args.empty?
|
|
61
|
+
|
|
62
|
+
res
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def parse_arg(res, arg)
|
|
68
|
+
if arg.first == :symbol_literal
|
|
69
|
+
res.add_tag parse_literal(arg)
|
|
70
|
+
elsif arg.first == :bare_assoc_hash
|
|
71
|
+
parse_hash(res, arg[1])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_hash(res, hash_arg)
|
|
76
|
+
hash_arg.each do |(_, label, val)|
|
|
77
|
+
res.add_htag label[1][0..-2].to_sym, parse_value(val)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Expr of the form:
|
|
82
|
+
# bool - [:var_ref, [:@kw, "true", [1, 24]]]
|
|
83
|
+
# string - [:string_literal, [:string_content, [...]]]
|
|
84
|
+
# int - [:@int, "3", [1, 52]]]]
|
|
85
|
+
def parse_value(expr)
|
|
86
|
+
case expr.first
|
|
87
|
+
when :var_ref
|
|
88
|
+
expr[1][1] == "true"
|
|
89
|
+
when :@int
|
|
90
|
+
expr[1].to_i
|
|
91
|
+
when :@float
|
|
92
|
+
expr[1].to_f
|
|
93
|
+
else
|
|
94
|
+
parse_literal(expr)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Expr of the form:
|
|
99
|
+
# [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
|
|
100
|
+
def parse_literal(expr)
|
|
101
|
+
val = expr[1][1][1]
|
|
102
|
+
val = val.to_sym if expr[0] == :symbol_literal ||
|
|
103
|
+
expr[0] == :assoc_new
|
|
104
|
+
val
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Expr of the form:
|
|
108
|
+
# [:var_ref, [:@const, "User", [1, 9]]]
|
|
109
|
+
#
|
|
110
|
+
# or
|
|
111
|
+
#
|
|
112
|
+
# [:const_path_ref, [:const_path_ref, [:var_ref,
|
|
113
|
+
# [:@const, "User", [1, 17]]],
|
|
114
|
+
# [:@const, "Guest", [1, 23]]],
|
|
115
|
+
# [:@const, "Collection", [1, 30]]
|
|
116
|
+
def parse_const(expr)
|
|
117
|
+
if expr.first == :var_ref
|
|
118
|
+
expr[1][1]
|
|
119
|
+
elsif expr.first == :@const
|
|
120
|
+
expr[1]
|
|
121
|
+
elsif expr.first == :const_path_ref
|
|
122
|
+
expr[1..].map(&method(:parse_const)).join("::")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ripper"
|
|
4
|
-
|
|
5
3
|
module TestProf
|
|
6
4
|
module RSpecStamp
|
|
7
5
|
# Parse examples headers
|
|
@@ -27,126 +25,16 @@ module TestProf
|
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# [
|
|
38
|
-
# [
|
|
39
|
-
# :command,
|
|
40
|
-
# [:@ident, "it", [1, 0]],
|
|
41
|
-
# [:args_add_block, [ ... ]]
|
|
42
|
-
# ]
|
|
43
|
-
# ]
|
|
44
|
-
# ]
|
|
45
|
-
#
|
|
46
|
-
# or
|
|
47
|
-
#
|
|
48
|
-
# [:program,
|
|
49
|
-
# [
|
|
50
|
-
# [
|
|
51
|
-
# :vcall,
|
|
52
|
-
# [:@ident, "it", [1, 0]]
|
|
53
|
-
# ]
|
|
54
|
-
# ]
|
|
55
|
-
# ]
|
|
56
|
-
res = Result.new
|
|
57
|
-
|
|
58
|
-
fcall = sexp[1][0][1]
|
|
59
|
-
args_block = sexp[1][0][2]
|
|
60
|
-
|
|
61
|
-
if fcall.first == :fcall
|
|
62
|
-
fcall = fcall[1]
|
|
63
|
-
elsif fcall.first == :var_ref
|
|
64
|
-
res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
|
|
65
|
-
args_block = sexp[1][0][4]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
res.fname ||= fcall[1]
|
|
69
|
-
|
|
70
|
-
return res if args_block.nil?
|
|
71
|
-
|
|
72
|
-
args_block = args_block[1] if args_block.first == :arg_paren
|
|
73
|
-
|
|
74
|
-
args = args_block[1]
|
|
75
|
-
|
|
76
|
-
if args.first.first == :string_literal
|
|
77
|
-
res.desc = parse_literal(args.shift)
|
|
78
|
-
elsif args.first.first == :var_ref || args.first.first == :const_path_ref
|
|
79
|
-
res.desc_const = parse_const(args.shift)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
parse_arg(res, args.shift) until args.empty?
|
|
83
|
-
|
|
84
|
-
res
|
|
28
|
+
instance =
|
|
29
|
+
begin
|
|
30
|
+
require_relative "parser/prism"
|
|
31
|
+
self::Prism.new
|
|
32
|
+
rescue LoadError
|
|
33
|
+
require_relative "parser/ripper"
|
|
34
|
+
self::Ripper.new
|
|
85
35
|
end
|
|
86
|
-
# rubocop: enable Metrics/CyclomaticComplexity
|
|
87
|
-
# rubocop: enable Metrics/PerceivedComplexity
|
|
88
36
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def parse_arg(res, arg)
|
|
92
|
-
if arg.first == :symbol_literal
|
|
93
|
-
res.add_tag parse_literal(arg)
|
|
94
|
-
elsif arg.first == :bare_assoc_hash
|
|
95
|
-
parse_hash(res, arg[1])
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def parse_hash(res, hash_arg)
|
|
100
|
-
hash_arg.each do |(_, label, val)|
|
|
101
|
-
res.add_htag label[1][0..-2].to_sym, parse_value(val)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Expr of the form:
|
|
106
|
-
# bool - [:var_ref, [:@kw, "true", [1, 24]]]
|
|
107
|
-
# string - [:string_literal, [:string_content, [...]]]
|
|
108
|
-
# int - [:@int, "3", [1, 52]]]]
|
|
109
|
-
def parse_value(expr)
|
|
110
|
-
case expr.first
|
|
111
|
-
when :var_ref
|
|
112
|
-
expr[1][1] == "true"
|
|
113
|
-
when :@int
|
|
114
|
-
expr[1].to_i
|
|
115
|
-
when :@float
|
|
116
|
-
expr[1].to_f
|
|
117
|
-
else
|
|
118
|
-
parse_literal(expr)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Expr of the form:
|
|
123
|
-
# [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
|
|
124
|
-
def parse_literal(expr)
|
|
125
|
-
val = expr[1][1][1]
|
|
126
|
-
val = val.to_sym if expr[0] == :symbol_literal ||
|
|
127
|
-
expr[0] == :assoc_new
|
|
128
|
-
val
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Expr of the form:
|
|
132
|
-
# [:var_ref, [:@const, "User", [1, 9]]]
|
|
133
|
-
#
|
|
134
|
-
# or
|
|
135
|
-
#
|
|
136
|
-
# [:const_path_ref, [:const_path_ref, [:var_ref,
|
|
137
|
-
# [:@const, "User", [1, 17]]],
|
|
138
|
-
# [:@const, "Guest", [1, 23]]],
|
|
139
|
-
# [:@const, "Collection", [1, 30]]
|
|
140
|
-
def parse_const(expr)
|
|
141
|
-
if expr.first == :var_ref
|
|
142
|
-
expr[1][1]
|
|
143
|
-
elsif expr.first == :@const
|
|
144
|
-
expr[1]
|
|
145
|
-
elsif expr.first == :const_path_ref
|
|
146
|
-
expr[1..].map(&method(:parse_const)).join("::")
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
37
|
+
define_singleton_method(:parse) { |code| instance.parse(code) }
|
|
150
38
|
end
|
|
151
39
|
end
|
|
152
40
|
end
|
data/lib/test_prof/rubocop.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
unless supported
|
|
6
|
-
warn "TestProf cops require RuboCop >= 0.51.0 to run."
|
|
7
|
-
return
|
|
8
|
-
end
|
|
3
|
+
warn <<~MSG
|
|
4
|
+
!!!
|
|
9
5
|
|
|
10
|
-
|
|
6
|
+
Please, update your .rubocop.yml configuration to load TestProf plugin as follows (and fix the error below):
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
plugins:
|
|
9
|
+
- test-prof
|
|
10
|
+
|
|
11
|
+
!!!
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MSG
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "test_prof/utils/sized_ordered_set"
|
|
4
|
+
require "forwardable"
|
|
4
5
|
|
|
5
6
|
module TestProf
|
|
6
7
|
module TPSProf
|
|
7
8
|
class Profiler
|
|
8
|
-
|
|
9
|
+
extend Forwardable
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
attr_reader :top_count, :groups, :total_count, :total_time,
|
|
12
|
+
:config
|
|
13
|
+
|
|
14
|
+
def_delegators :@config, :min_examples_count, :min_group_time, :min_target_tps,
|
|
15
|
+
:mode, :max_examples_count, :max_group_time, :min_tps
|
|
16
|
+
|
|
17
|
+
def initialize(top_count, config)
|
|
18
|
+
@config = config
|
|
19
|
+
# In strict mode, we use the sorted set to keep track of offenders
|
|
20
|
+
# to show in the end
|
|
21
|
+
@top_count = (config.mode == :strict) ? 100 : top_count
|
|
13
22
|
@total_count = 0
|
|
14
23
|
@total_time = 0.0
|
|
15
|
-
@groups = Utils::SizedOrderedSet.new(top_count, sort_by: :
|
|
24
|
+
@groups = Utils::SizedOrderedSet.new(top_count, sort_by: :penalty)
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def group_started(id)
|
|
@@ -22,20 +31,48 @@ module TestProf
|
|
|
22
31
|
@group_started_at = TestProf.now
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
def group_finished(
|
|
26
|
-
return unless @examples_count >=
|
|
34
|
+
def group_finished(group)
|
|
35
|
+
return unless @examples_count >= min_examples_count
|
|
36
|
+
|
|
37
|
+
total_time = TestProf.now - @group_started_at
|
|
38
|
+
shared_setup_time = total_time - @examples_time
|
|
39
|
+
|
|
40
|
+
return unless total_time >= min_group_time
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
group_time = (TestProf.now - @group_started_at) - @examples_time
|
|
30
|
-
run_time = @examples_time + group_time
|
|
42
|
+
tps = (@examples_count / total_time).round(2)
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
return unless tps < min_target_tps
|
|
45
|
+
|
|
46
|
+
# How much time did we waste compared to the target TPS
|
|
47
|
+
penalty = @examples_count * ((1.0 / tps) - (1.0 / min_target_tps))
|
|
48
|
+
|
|
49
|
+
item = {
|
|
50
|
+
id: group,
|
|
51
|
+
total_time: total_time,
|
|
52
|
+
shared_setup_time: shared_setup_time,
|
|
36
53
|
count: @examples_count,
|
|
37
|
-
tps:
|
|
54
|
+
tps: tps,
|
|
55
|
+
penalty: penalty
|
|
38
56
|
}
|
|
57
|
+
|
|
58
|
+
if mode == :strict
|
|
59
|
+
location = group.metadata[:location]
|
|
60
|
+
|
|
61
|
+
if TPSProf.handle_group_strictly(
|
|
62
|
+
GroupInfo.new(
|
|
63
|
+
group: group,
|
|
64
|
+
location: location,
|
|
65
|
+
examples_count: @examples_count,
|
|
66
|
+
total_time: total_time,
|
|
67
|
+
tps: tps,
|
|
68
|
+
penalty: penalty
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
groups << item
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
groups << item
|
|
75
|
+
end
|
|
39
76
|
end
|
|
40
77
|
|
|
41
78
|
def example_started(id)
|
|
@@ -23,25 +23,45 @@ module TestProf
|
|
|
23
23
|
<<~MSG
|
|
24
24
|
Total TPS (tests per second): #{total_tps}
|
|
25
25
|
|
|
26
|
-
Top #{profiler.top_count} slowest suites by TPS (tests per second) (min examples per group: #{profiler.threshold}):
|
|
27
|
-
|
|
28
26
|
MSG
|
|
29
27
|
|
|
28
|
+
if profiler.mode == :strict
|
|
29
|
+
return if groups.empty?
|
|
30
|
+
|
|
31
|
+
msgs << if groups.size < profiler.top_count
|
|
32
|
+
<<~MSG
|
|
33
|
+
Suites violating TPS limits:
|
|
34
|
+
|
|
35
|
+
MSG
|
|
36
|
+
else
|
|
37
|
+
<<~MSG
|
|
38
|
+
Top #{profiler.top_count} suites violating TPS limits:
|
|
39
|
+
|
|
40
|
+
MSG
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
msgs <<
|
|
44
|
+
<<~MSG
|
|
45
|
+
Top #{profiler.top_count} slowest suites by TPS (tests per second):
|
|
46
|
+
|
|
47
|
+
MSG
|
|
48
|
+
end
|
|
49
|
+
|
|
30
50
|
groups.each do |group|
|
|
31
51
|
description = group[:id].top_level_description
|
|
32
52
|
location = group[:id].metadata[:location]
|
|
33
|
-
time = group[:
|
|
34
|
-
|
|
53
|
+
time = group[:total_time]
|
|
54
|
+
setup_time = group[:shared_setup_time]
|
|
35
55
|
count = group[:count]
|
|
36
|
-
tps =
|
|
56
|
+
tps = group[:tps]
|
|
37
57
|
|
|
38
58
|
msgs <<
|
|
39
59
|
<<~GROUP
|
|
40
|
-
#{description.truncate} (#{location}) – #{tps} TPS (#{time.duration} / #{count}
|
|
60
|
+
#{description.truncate} (#{location}) – #{tps} TPS (#{time.duration} / #{count}, shared setup time: #{setup_time.duration})
|
|
41
61
|
GROUP
|
|
42
62
|
end
|
|
43
63
|
|
|
44
|
-
log :info, msgs.join
|
|
64
|
+
log((profiler.mode == :strict) ? :error : :info, msgs.join)
|
|
45
65
|
end
|
|
46
66
|
end
|
|
47
67
|
end
|
|
@@ -15,27 +15,50 @@ module TestProf
|
|
|
15
15
|
attr_reader :reporter, :profiler
|
|
16
16
|
|
|
17
17
|
def initialize
|
|
18
|
-
@profiler = Profiler.new(TPSProf.config.top_count,
|
|
18
|
+
@profiler = Profiler.new(TPSProf.config.top_count, TPSProf.config)
|
|
19
19
|
@reporter = TPSProf.config.reporter
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
conf = TPSProf.config
|
|
22
|
+
if conf.mode == :strict
|
|
23
|
+
config_parts = []
|
|
24
|
+
|
|
25
|
+
if conf.custom_strict_handler
|
|
26
|
+
config_parts << "custom handler"
|
|
27
|
+
else
|
|
28
|
+
config_parts << "max examples: #{conf.max_examples_count}" if conf.max_examples_count
|
|
29
|
+
config_parts << "max group time: #{conf.max_group_time}" if conf.max_group_time
|
|
30
|
+
config_parts << "min tps: #{conf.min_tps}" if conf.min_tps
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
log :info, "TPSProf strict enabled (#{config_parts.join(", ")})"
|
|
34
|
+
else
|
|
35
|
+
log :info, "TPSProf enabled (top-#{TPSProf.config.top_count})"
|
|
36
|
+
end
|
|
22
37
|
end
|
|
23
38
|
|
|
24
39
|
def example_group_started(notification)
|
|
25
40
|
return unless notification.group.top_level?
|
|
41
|
+
return if notification.group.metadata[:tps_prof] == :ignore
|
|
42
|
+
|
|
26
43
|
profiler.group_started notification.group
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def example_group_finished(notification)
|
|
30
47
|
return unless notification.group.top_level?
|
|
48
|
+
return if notification.group.metadata[:tps_prof] == :ignore
|
|
49
|
+
|
|
31
50
|
profiler.group_finished notification.group
|
|
32
51
|
end
|
|
33
52
|
|
|
34
53
|
def example_started(notification)
|
|
54
|
+
return if notification.example.metadata[:tps_prof] == :ignore
|
|
55
|
+
|
|
35
56
|
profiler.example_started notification.example
|
|
36
57
|
end
|
|
37
58
|
|
|
38
59
|
def example_finished(notification)
|
|
60
|
+
return if notification.example.metadata[:tps_prof] == :ignore
|
|
61
|
+
|
|
39
62
|
profiler.example_finished notification.example
|
|
40
63
|
end
|
|
41
64
|
|
data/lib/test_prof/tps_prof.rb
CHANGED
|
@@ -8,25 +8,81 @@ module TestProf
|
|
|
8
8
|
#
|
|
9
9
|
# Example:
|
|
10
10
|
#
|
|
11
|
+
# # Show top-10 groups with the worst TPS
|
|
11
12
|
# TPS_PROF=10 rspec ...
|
|
12
13
|
#
|
|
14
|
+
# # Report (as errors) groups with lower TPS
|
|
15
|
+
# TPS_PROF_MIN_TPS=10 TPS_PROF=strict rspec ...
|
|
13
16
|
module TPSProf
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
class GroupInfo < Struct.new(:group, :location, :examples_count, :total_time, :tps, :penalty, keyword_init: true)
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
class Configuration
|
|
15
|
-
attr_accessor :top_count, :
|
|
23
|
+
attr_accessor :top_count, :reporter, :mode
|
|
24
|
+
|
|
25
|
+
# Thresholds
|
|
26
|
+
attr_accessor(
|
|
27
|
+
:min_examples_count, # Ignore groups with fewer examples
|
|
28
|
+
:min_group_time, # Ignore groups with less total time (in seconds)
|
|
29
|
+
:min_target_tps, # Ignore groups with higher TPS
|
|
30
|
+
:max_examples_count, # Report groups with more examples in strict mode
|
|
31
|
+
:max_group_time, # Report groups with more total time in strict mode
|
|
32
|
+
:min_tps # Report groups with lower TPS in strict mode
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
attr_reader :custom_strict_handler
|
|
16
36
|
|
|
17
37
|
def initialize
|
|
18
|
-
@
|
|
19
|
-
@top_count =
|
|
20
|
-
|
|
38
|
+
@mode = (ENV["TPS_PROF_MODE"] || ((ENV["TPS_PROF"] == "strict") ? :strict : :profile)).to_sym
|
|
39
|
+
@top_count = ENV["TPS_PROF_COUNT"]&.to_i || ((ENV["TPS_PROF"].to_i > 1) ? ENV["TPS_PROF"].to_i : nil) || 10
|
|
40
|
+
|
|
41
|
+
@min_examples_count = ENV.fetch("TPS_PROF_MIN_EXAMPLES", 10).to_i
|
|
42
|
+
@min_group_time = ENV.fetch("TPS_PROF_MIN_TIME", 5).to_i
|
|
43
|
+
@min_target_tps = ENV.fetch("TPS_PROF_TARGET_TPS", 30).to_i
|
|
44
|
+
|
|
45
|
+
@max_examples_count = ENV["TPS_PROF_MAX_EXAMPLES"]&.to_i
|
|
46
|
+
@max_group_time = ENV["TPS_PROF_MAX_TIME"]&.to_i
|
|
47
|
+
@min_tps = ENV["TPS_PROF_MIN_TPS"]&.to_i
|
|
48
|
+
|
|
21
49
|
@reporter = resolve_reporter(ENV["TPS_PROF_FORMAT"])
|
|
22
50
|
end
|
|
23
51
|
|
|
52
|
+
def strict_handler
|
|
53
|
+
@strict_handler ||= method(:default_strict_handler)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def strict_handler=(val)
|
|
57
|
+
@strict_handler = val
|
|
58
|
+
@custom_strict_handler = true
|
|
59
|
+
end
|
|
60
|
+
|
|
24
61
|
private
|
|
25
62
|
|
|
26
63
|
def resolve_reporter(format)
|
|
27
64
|
# TODO: support other formats
|
|
28
65
|
TPSProf::Reporter::Text.new
|
|
29
66
|
end
|
|
67
|
+
|
|
68
|
+
def default_strict_handler(group_info)
|
|
69
|
+
error_msg = nil
|
|
70
|
+
location = group_info.location
|
|
71
|
+
|
|
72
|
+
if max_examples_count && group_info.examples_count > max_examples_count
|
|
73
|
+
error_msg ||= "Group #{location} has too many examples: #{group_info.examples_count} > #{max_examples_count}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if max_group_time && group_info.total_time > max_group_time
|
|
77
|
+
error_msg ||= "Group #{location} has too long total time: #{group_info.total_time} > #{max_group_time}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if min_tps && group_info.tps < min_tps
|
|
81
|
+
error_msg ||= "Group #{location} has too low TPS: #{group_info.tps} < #{min_tps}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise error_msg if error_msg
|
|
85
|
+
end
|
|
30
86
|
end
|
|
31
87
|
|
|
32
88
|
class << self
|
|
@@ -37,6 +93,15 @@ module TestProf
|
|
|
37
93
|
def configure
|
|
38
94
|
yield config
|
|
39
95
|
end
|
|
96
|
+
|
|
97
|
+
def handle_group_strictly(group_info)
|
|
98
|
+
reporter = ::RSpec.configuration.reporter
|
|
99
|
+
config.strict_handler.call(group_info)
|
|
100
|
+
false
|
|
101
|
+
rescue => err
|
|
102
|
+
reporter.notify_non_example_exception(Error.new(err.message), "")
|
|
103
|
+
true
|
|
104
|
+
end
|
|
40
105
|
end
|
|
41
106
|
end
|
|
42
107
|
end
|
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.6.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:
|
|
11
|
+
date: 2026-03-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -123,6 +123,15 @@ files:
|
|
|
123
123
|
- lib/minitest/base_reporter.rb
|
|
124
124
|
- lib/minitest/event_prof_formatter.rb
|
|
125
125
|
- lib/minitest/test_prof_plugin.rb
|
|
126
|
+
- lib/rubocop/test_prof.rb
|
|
127
|
+
- lib/rubocop/test_prof/cops/rspec/aggregate_examples.rb
|
|
128
|
+
- lib/rubocop/test_prof/cops/rspec/aggregate_examples/its.rb
|
|
129
|
+
- lib/rubocop/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb
|
|
130
|
+
- lib/rubocop/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb
|
|
131
|
+
- lib/rubocop/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb
|
|
132
|
+
- lib/rubocop/test_prof/cops/rspec/aggregate_examples/node_matchers.rb
|
|
133
|
+
- lib/rubocop/test_prof/cops/rspec/language.rb
|
|
134
|
+
- lib/rubocop/test_prof/plugin.rb
|
|
126
135
|
- lib/test-prof.rb
|
|
127
136
|
- lib/test_prof.rb
|
|
128
137
|
- lib/test_prof/any_fixture.rb
|
|
@@ -135,14 +144,6 @@ files:
|
|
|
135
144
|
- lib/test_prof/before_all.rb
|
|
136
145
|
- lib/test_prof/before_all/adapters/active_record.rb
|
|
137
146
|
- lib/test_prof/before_all/isolator.rb
|
|
138
|
-
- lib/test_prof/cops/plugin.rb
|
|
139
|
-
- lib/test_prof/cops/rspec/aggregate_examples.rb
|
|
140
|
-
- lib/test_prof/cops/rspec/aggregate_examples/its.rb
|
|
141
|
-
- lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb
|
|
142
|
-
- lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb
|
|
143
|
-
- lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb
|
|
144
|
-
- lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb
|
|
145
|
-
- lib/test_prof/cops/rspec/language.rb
|
|
146
147
|
- lib/test_prof/core.rb
|
|
147
148
|
- lib/test_prof/event_prof.rb
|
|
148
149
|
- lib/test_prof/event_prof/custom_events.rb
|
|
@@ -200,12 +201,12 @@ files:
|
|
|
200
201
|
- lib/test_prof/recipes/rspec/let_it_be.rb
|
|
201
202
|
- lib/test_prof/recipes/rspec/sample.rb
|
|
202
203
|
- lib/test_prof/rspec_dissect.rb
|
|
203
|
-
- lib/test_prof/rspec_dissect/
|
|
204
|
-
- lib/test_prof/rspec_dissect/collectors/before.rb
|
|
205
|
-
- lib/test_prof/rspec_dissect/collectors/let.rb
|
|
204
|
+
- lib/test_prof/rspec_dissect/collector.rb
|
|
206
205
|
- lib/test_prof/rspec_dissect/rspec.rb
|
|
207
206
|
- lib/test_prof/rspec_stamp.rb
|
|
208
207
|
- lib/test_prof/rspec_stamp/parser.rb
|
|
208
|
+
- lib/test_prof/rspec_stamp/parser/prism.rb
|
|
209
|
+
- lib/test_prof/rspec_stamp/parser/ripper.rb
|
|
209
210
|
- lib/test_prof/rspec_stamp/rspec.rb
|
|
210
211
|
- lib/test_prof/rubocop.rb
|
|
211
212
|
- lib/test_prof/ruby_prof.rb
|