test-prof 0.11.0 → 0.12.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 +109 -446
- data/LICENSE.txt +1 -1
- data/README.md +9 -13
- data/config/default.yml +35 -0
- data/lib/minitest/test_prof_plugin.rb +3 -0
- data/lib/test_prof/before_all.rb +0 -4
- data/lib/test_prof/cops/inject.rb +16 -14
- data/lib/test_prof/cops/rspec/aggregate_examples.rb +2 -2
- data/lib/test_prof/cops/rspec/aggregate_examples/its.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +2 -4
- data/lib/test_prof/event_prof/instrumentations/active_support.rb +22 -4
- data/lib/test_prof/ext/active_record_3.rb +1 -1
- data/lib/test_prof/recipes/minitest/sample.rb +6 -10
- data/lib/test_prof/recipes/rspec/before_all.rb +1 -9
- data/lib/test_prof/recipes/rspec/let_it_be.rb +108 -11
- data/lib/test_prof/recipes/rspec/sample.rb +4 -2
- data/lib/test_prof/stack_prof.rb +3 -0
- data/lib/test_prof/version.rb +1 -1
- metadata +16 -15
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
[![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](
|
2
|
-
[![Gem Version](https://badge.fury.io/rb/test-prof.svg)](https://rubygems.org/gems/test-prof) [![Build](https://github.com/
|
3
|
-
[![JRuby Build](https://github.com/
|
4
|
-
[![Code Triagers Badge](https://www.codetriage.com/
|
1
|
+
[![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com)
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/test-prof.svg)](https://rubygems.org/gems/test-prof) [![Build](https://github.com/test-prof/test-prof/workflows/Build/badge.svg)](https://github.com/test-prof/test-prof/actions)
|
3
|
+
[![JRuby Build](https://github.com/test-prof/test-prof/workflows/JRuby%20Build/badge.svg)](https://github.com/test-prof/test-prof/actions)
|
4
|
+
[![Code Triagers Badge](https://www.codetriage.com/test-prof/test-prof/badges/users.svg)](https://www.codetriage.com/test-prof/test-prof)
|
5
5
|
[![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://test-prof.evilmartians.io)
|
6
6
|
|
7
7
|
# Ruby Tests Profiling Toolbox
|
@@ -47,11 +47,11 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
|
|
47
47
|
## Who uses TestProf
|
48
48
|
|
49
49
|
- [Discourse](https://github.com/discourse/discourse) reduced [~27% of their test suite time](https://twitter.com/samsaffron/status/1125602558024699904)
|
50
|
-
- [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370)
|
50
|
+
- [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370) and [improved factories usage](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26810)
|
51
51
|
- [CodeTriage](https://github.com/codetriage/codetriage)
|
52
52
|
- [Dev.to](https://github.com/thepracticaldev/dev.to)
|
53
53
|
- [Open Project](https://github.com/opf/openproject)
|
54
|
-
- [...and others](https://github.com/
|
54
|
+
- [...and others](https://github.com/test-prof/test-prof/issues/73)
|
55
55
|
|
56
56
|
## Resources
|
57
57
|
|
@@ -83,7 +83,7 @@ And that's it)
|
|
83
83
|
|
84
84
|
Supported Ruby versions:
|
85
85
|
|
86
|
-
- Ruby (MRI) >= 2.
|
86
|
+
- Ruby (MRI) >= 2.5.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)
|
87
87
|
|
88
88
|
- JRuby >= 9.1.0.0 (**NOTE:** refinements-dependent features might require 9.2.7+)
|
89
89
|
|
@@ -95,16 +95,12 @@ Check out our [docs][].
|
|
95
95
|
|
96
96
|
## What's next?
|
97
97
|
|
98
|
-
Have an idea? [Propose](https://github.com/
|
98
|
+
Have an idea? [Propose](https://github.com/test-prof/test-prof/issues/new) a feature request!
|
99
99
|
|
100
|
-
Already using TestProf? [Share your story!](https://github.com/
|
100
|
+
Already using TestProf? [Share your story!](https://github.com/test-prof/test-prof/issues/73)
|
101
101
|
|
102
102
|
## License
|
103
103
|
|
104
104
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
105
105
|
|
106
106
|
[docs]: https://test-prof.evilmartians.io
|
107
|
-
|
108
|
-
## Security Contact
|
109
|
-
|
110
|
-
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
|
data/config/default.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
RSpec:
|
4
|
+
Patterns:
|
5
|
+
- _spec.rb
|
6
|
+
- "(?:^|/)spec/"
|
7
|
+
|
8
|
+
RSpec/AggregateExamples:
|
9
|
+
Description: Checks if example group contains two or more aggregatable examples.
|
10
|
+
Enabled: true
|
11
|
+
StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
|
12
|
+
AddAggregateFailuresMetadata: true
|
13
|
+
MatchersWithSideEffects:
|
14
|
+
- allow_value
|
15
|
+
- allow_values
|
16
|
+
- validate_presence_of
|
17
|
+
- validate_absence_of
|
18
|
+
- validate_length_of
|
19
|
+
- validate_inclusion_of
|
20
|
+
- validates_exclusion_of
|
21
|
+
|
22
|
+
# TODO: remove this one we hit 1.0
|
23
|
+
RSpec/AggregateFailures:
|
24
|
+
Description: Checks if example group contains two or more aggregatable examples.
|
25
|
+
Enabled: false
|
26
|
+
StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
|
27
|
+
AddAggregateFailuresMetadata: true
|
28
|
+
MatchersWithSideEffects:
|
29
|
+
- allow_value
|
30
|
+
- allow_values
|
31
|
+
- validate_presence_of
|
32
|
+
- validate_absence_of
|
33
|
+
- validate_length_of
|
34
|
+
- validate_inclusion_of
|
35
|
+
- validates_exclusion_of
|
@@ -12,6 +12,7 @@ module Minitest # :nodoc:
|
|
12
12
|
opts[:top_count] = ENV["EVENT_PROF_TOP"].to_i if ENV["EVENT_PROF_TOP"]
|
13
13
|
opts[:per_example] = true if ENV["EVENT_PROF_EXAMPLES"]
|
14
14
|
opts[:fdoc] = true if ENV["FDOC"]
|
15
|
+
opts[:sample] = true if ENV["SAMPLE"] || ENV["SAMPLE_GROUPS"]
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -39,5 +40,7 @@ module Minitest # :nodoc:
|
|
39
40
|
|
40
41
|
reporter << TestProf::EventProfReporter.new(options[:io], options) if options[:event]
|
41
42
|
reporter << TestProf::FactoryDoctorReporter.new(options[:io], options) if options[:fdoc]
|
43
|
+
|
44
|
+
::TestProf::MinitestSample.call if options[:sample]
|
42
45
|
end
|
43
46
|
end
|
data/lib/test_prof/before_all.rb
CHANGED
@@ -2,22 +2,24 @@
|
|
2
2
|
|
3
3
|
# This is shamelessly borrowed from RuboCop RSpec
|
4
4
|
# https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
module TestProf
|
6
|
+
module Cops
|
7
|
+
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
|
8
|
+
# bit of our configuration.
|
9
|
+
module Inject
|
10
|
+
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.parent.expand_path.freeze
|
11
|
+
CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
def self.defaults!
|
14
|
+
path = CONFIG_DEFAULT.to_s
|
15
|
+
hash = RuboCop::ConfigLoader.send(:load_yaml_configuration, path)
|
16
|
+
config = RuboCop::Config.new(hash, path)
|
17
|
+
puts "configuration from #{path}" if RuboCop::ConfigLoader.debug?
|
18
|
+
config = RuboCop::ConfigLoader.merge_with_default(config, path)
|
19
|
+
RuboCop::ConfigLoader.instance_variable_set(:@default_configuration, config)
|
20
|
+
end
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
|
-
|
25
|
+
TestProf::Cops::Inject.defaults!
|
@@ -12,7 +12,7 @@ module RuboCop
|
|
12
12
|
module RSpec
|
13
13
|
# Checks if example groups contain two or more aggregatable examples.
|
14
14
|
#
|
15
|
-
# @see https://github.com/rubocop-hq/rspec-style-guide#
|
15
|
+
# @see https://github.com/rubocop-hq/rspec-style-guide#expectation-per-example
|
16
16
|
#
|
17
17
|
# This cop is primarily for reducing the cost of repeated expensive
|
18
18
|
# context initialization.
|
@@ -108,7 +108,7 @@ module RuboCop
|
|
108
108
|
# expect(number).to be_odd
|
109
109
|
# end
|
110
110
|
#
|
111
|
-
class AggregateExamples < Cop
|
111
|
+
class AggregateExamples < ::RuboCop::Cop::Cop
|
112
112
|
include LineRangeHelpers
|
113
113
|
include MetadataHelpers
|
114
114
|
include NodeMatchers
|
@@ -5,7 +5,7 @@ require_relative "../language"
|
|
5
5
|
module RuboCop
|
6
6
|
module Cop
|
7
7
|
module RSpec
|
8
|
-
class AggregateExamples < Cop
|
8
|
+
class AggregateExamples < ::RuboCop::Cop::Cop
|
9
9
|
# When aggregated, the expectations will fail when not supposed to or
|
10
10
|
# have a risk of not failing when expected to. One example is
|
11
11
|
# `validate_presence_of :comment` as it leaves an empty comment after
|
@@ -4,14 +4,12 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
class AggregateExamples
|
7
|
-
def self.
|
8
|
-
|
7
|
+
def self.registry
|
8
|
+
RuboCop::Cop::Cop.registry
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
class AggregateFailures < AggregateExamples
|
13
|
-
raise "Remove me" if TestProf::VERSION >= "1.0"
|
14
|
-
|
15
13
|
def initialize(*)
|
16
14
|
super
|
17
15
|
self.class.just_once { warn "`AggregateFailures` cop has been renamed to `AggregateExamples`." }
|
@@ -4,13 +4,31 @@ module TestProf::EventProf
|
|
4
4
|
module Instrumentations
|
5
5
|
# Wrapper over ActiveSupport::Notifications
|
6
6
|
module ActiveSupport
|
7
|
+
class Subscriber
|
8
|
+
attr_reader :block, :started_at
|
9
|
+
|
10
|
+
def initialize(block)
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def start(*)
|
15
|
+
@started_at = TestProf.now
|
16
|
+
end
|
17
|
+
|
18
|
+
def publish(_name, started_at, finished_at, *)
|
19
|
+
block.call(finished_at - started_at)
|
20
|
+
end
|
21
|
+
|
22
|
+
def finish(*)
|
23
|
+
block.call(TestProf.now - started_at)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
7
27
|
class << self
|
8
|
-
def subscribe(event)
|
28
|
+
def subscribe(event, &block)
|
9
29
|
raise ArgumentError, "Block is required!" unless block_given?
|
10
30
|
|
11
|
-
::ActiveSupport::Notifications.subscribe(event
|
12
|
-
yield (finish - start)
|
13
|
-
end
|
31
|
+
::ActiveSupport::Notifications.subscribe(event, Subscriber.new(block))
|
14
32
|
end
|
15
33
|
|
16
34
|
def instrument(event)
|
@@ -5,8 +5,8 @@ module TestProf
|
|
5
5
|
module ActiveRecord3Transactions
|
6
6
|
refine ::ActiveRecord::ConnectionAdapters::AbstractAdapter do
|
7
7
|
def begin_transaction(joinable: true)
|
8
|
-
increment_open_transactions
|
9
8
|
if open_transactions > 0
|
9
|
+
increment_open_transactions
|
10
10
|
create_savepoint
|
11
11
|
else
|
12
12
|
begin_db_transaction
|
@@ -35,18 +35,14 @@ module TestProf
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
|
-
end
|
39
38
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
39
|
+
def call
|
40
|
+
if ENV["SAMPLE"]
|
41
|
+
::TestProf::MinitestSample.sample_examples(ENV["SAMPLE"].to_i)
|
42
|
+
elsif ENV["SAMPLE_GROUPS"]
|
43
|
+
::TestProf::MinitestSample.sample_groups(ENV["SAMPLE_GROUPS"].to_i)
|
44
|
+
end
|
46
45
|
end
|
47
|
-
super
|
48
46
|
end
|
49
47
|
end
|
50
48
|
end
|
51
|
-
|
52
|
-
Minitest.singleton_class.prepend(TestProf::MinitestSample)
|
@@ -9,7 +9,7 @@ module TestProf
|
|
9
9
|
def before_all(&block)
|
10
10
|
raise ArgumentError, "Block is required!" unless block_given?
|
11
11
|
|
12
|
-
return
|
12
|
+
return before(:all, &block) if within_before_all?
|
13
13
|
|
14
14
|
@__before_all_activated__ = true
|
15
15
|
|
@@ -24,14 +24,6 @@ module TestProf
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def within_before_all(&block)
|
28
|
-
before(:all) do
|
29
|
-
BeforeAll.within_transaction do
|
30
|
-
instance_eval(&block)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
27
|
def within_before_all?
|
36
28
|
instance_variable_defined?(:@__before_all_activated__)
|
37
29
|
end
|
@@ -22,6 +22,10 @@ module TestProf
|
|
22
22
|
|
23
23
|
LetItBe.modifiers[key] = block
|
24
24
|
end
|
25
|
+
|
26
|
+
def default_modifiers
|
27
|
+
@default_modifiers ||= {}
|
28
|
+
end
|
25
29
|
end
|
26
30
|
|
27
31
|
class << self
|
@@ -75,6 +79,8 @@ module TestProf
|
|
75
79
|
# And we love cats!)
|
76
80
|
PREFIX = RUBY_ENGINE == "jruby" ? "@__jruby_is_not_cat_friendly__" : "@😸"
|
77
81
|
|
82
|
+
FROZEN_ERROR_HINT = "\nIf you are using `let_it_be`, you may want to pass `reload: true` or `refind: true` modifier to it."
|
83
|
+
|
78
84
|
def self.define_let_it_be_alias(name, **default_args)
|
79
85
|
define_method(name) do |identifier, **options, &blk|
|
80
86
|
let_it_be(identifier, **default_args.merge(options), &blk)
|
@@ -83,20 +89,20 @@ module TestProf
|
|
83
89
|
|
84
90
|
def let_it_be(identifier, **options, &block)
|
85
91
|
initializer = proc do
|
86
|
-
instance_variable_set(:"#{PREFIX}#{identifier}", instance_exec(&block))
|
92
|
+
instance_variable_set(:"#{TestProf::LetItBe::PREFIX}#{identifier}", instance_exec(&block))
|
93
|
+
rescue FrozenError => e
|
94
|
+
e.message << TestProf::LetItBe::FROZEN_ERROR_HINT
|
95
|
+
raise
|
87
96
|
end
|
88
97
|
|
89
|
-
|
90
|
-
|
91
|
-
else
|
92
|
-
before_all(&initializer)
|
93
|
-
end
|
98
|
+
default_options = LetItBe.config.default_modifiers.dup
|
99
|
+
default_options.merge!(metadata[:let_it_be_modifiers]) if metadata[:let_it_be_modifiers]
|
94
100
|
|
95
|
-
|
96
|
-
|
101
|
+
options = default_options.merge(options)
|
102
|
+
|
103
|
+
before_all(&initializer)
|
97
104
|
|
98
|
-
|
99
|
-
let_accessor = LetItBe.wrap_with_modifiers(modifiers) do
|
105
|
+
let_accessor = LetItBe.wrap_with_modifiers(options) do
|
100
106
|
instance_variable_get(:"#{PREFIX}#{identifier}")
|
101
107
|
end
|
102
108
|
|
@@ -114,16 +120,78 @@ module TestProf
|
|
114
120
|
|
115
121
|
let(identifier, &let_accessor)
|
116
122
|
end
|
123
|
+
|
124
|
+
module Freezer
|
125
|
+
# Stoplist to prevent freezing objects and theirs associations that are defined
|
126
|
+
# with `let_it_be`'s `freeze: false` options during deep freezing.
|
127
|
+
#
|
128
|
+
# To only keep track of objects that are available in current example group,
|
129
|
+
# `begin` adds a new layer, and `rollback` removes a layer of unrelated objects
|
130
|
+
# along with rolling back the transaction where they were created.
|
131
|
+
#
|
132
|
+
# Stoplist holds records declared with `freeze: false` (so we do not freeze them even if they're used as
|
133
|
+
# associated records for frozen objects)
|
134
|
+
module Stoplist
|
135
|
+
class << self
|
136
|
+
def stop?(record)
|
137
|
+
@stoplist.any? { |layer| layer.include?(record) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def stop!(record)
|
141
|
+
@stoplist.last.push(record)
|
142
|
+
end
|
143
|
+
|
144
|
+
def begin
|
145
|
+
@stoplist.push([])
|
146
|
+
end
|
147
|
+
|
148
|
+
def rollback
|
149
|
+
@stoplist.pop
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Stack of example group-related variable definitions
|
154
|
+
@stoplist = []
|
155
|
+
end
|
156
|
+
|
157
|
+
class << self
|
158
|
+
# Rerucsively freezes the object to detect modifications
|
159
|
+
def deep_freeze(record)
|
160
|
+
return if record.frozen?
|
161
|
+
return if Stoplist.stop?(record)
|
162
|
+
|
163
|
+
record.freeze
|
164
|
+
|
165
|
+
# Support `let_it_be` with `create_list`
|
166
|
+
return record.each { |rec| deep_freeze(rec) } if record.respond_to?(:each)
|
167
|
+
|
168
|
+
# Freeze associations as well.
|
169
|
+
return unless defined?(::ActiveRecord::Base)
|
170
|
+
return unless record.is_a?(::ActiveRecord::Base)
|
171
|
+
|
172
|
+
record.class.reflections.keys.each do |reflection|
|
173
|
+
# But only if they are already loaded. If not yet loaded, they weren't
|
174
|
+
# created by factories, and it's ok to mutate them.
|
175
|
+
|
176
|
+
next unless record.association(reflection.to_sym).loaded?
|
177
|
+
|
178
|
+
target = record.association(reflection.to_sym).target
|
179
|
+
deep_freeze(target) if target.is_a?(::ActiveRecord::Base) || target.respond_to?(:each)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
117
184
|
end
|
118
185
|
end
|
119
186
|
|
120
|
-
if defined?(::ActiveRecord)
|
187
|
+
if defined?(::ActiveRecord::Base)
|
121
188
|
require "test_prof/ext/active_record_refind"
|
122
189
|
using TestProf::Ext::ActiveRecordRefind
|
123
190
|
|
124
191
|
TestProf::LetItBe.configure do |config|
|
125
192
|
config.register_modifier :reload do |record, val|
|
126
193
|
next record unless val
|
194
|
+
|
127
195
|
next record.reload if record.is_a?(::ActiveRecord::Base)
|
128
196
|
|
129
197
|
if record.respond_to?(:map)
|
@@ -136,6 +204,7 @@ if defined?(::ActiveRecord)
|
|
136
204
|
|
137
205
|
config.register_modifier :refind do |record, val|
|
138
206
|
next record unless val
|
207
|
+
|
139
208
|
next record.refind if record.is_a?(::ActiveRecord::Base)
|
140
209
|
|
141
210
|
if record.respond_to?(:map)
|
@@ -145,7 +214,35 @@ if defined?(::ActiveRecord)
|
|
145
214
|
end
|
146
215
|
record
|
147
216
|
end
|
217
|
+
|
218
|
+
config.register_modifier :freeze do |record, val|
|
219
|
+
if val == false
|
220
|
+
TestProf::LetItBe::Freezer::Stoplist.stop!(record)
|
221
|
+
next record
|
222
|
+
end
|
223
|
+
|
224
|
+
TestProf::LetItBe::Freezer.deep_freeze(record)
|
225
|
+
record
|
226
|
+
end
|
148
227
|
end
|
149
228
|
end
|
150
229
|
|
151
230
|
RSpec::Core::ExampleGroup.extend TestProf::LetItBe
|
231
|
+
|
232
|
+
TestProf::BeforeAll.configure do |config|
|
233
|
+
config.before(:begin) do
|
234
|
+
TestProf::LetItBe::Freezer::Stoplist.begin
|
235
|
+
end
|
236
|
+
|
237
|
+
config.after(:rollback) do
|
238
|
+
TestProf::LetItBe::Freezer::Stoplist.rollback
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
RSpec.configure do |config|
|
243
|
+
config.after(:example) do |example|
|
244
|
+
if example.exception&.is_a?(FrozenError)
|
245
|
+
example.exception.message << TestProf::LetItBe::FROZEN_ERROR_HINT
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|