test-prof 0.11.0 → 0.12.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.
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017-2019 palkan
3
+ Copyright (c) 2017-2020 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
- [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](http://cultofmartians.com)
2
- [![Gem Version](https://badge.fury.io/rb/test-prof.svg)](https://rubygems.org/gems/test-prof) [![Build](https://github.com/palkan/test-prof/workflows/Build/badge.svg)](https://github.com/palkan/test-prof/actions)
3
- [![JRuby Build](https://github.com/palkan/test-prof/workflows/JRuby%20Build/badge.svg)](https://github.com/palkan/test-prof/actions)
4
- [![Code Triagers Badge](https://www.codetriage.com/palkan/test-prof/badges/users.svg)](https://www.codetriage.com/palkan/test-prof)
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/palkan/test-prof/issues/73)
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.4.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0 or Ruby 2.3 use TestProf ~> 0.7.0)
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/palkan/test-prof/issues/new) a feature request!
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/palkan/test-prof/issues/73)
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.
@@ -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
@@ -24,10 +24,6 @@ module TestProf
24
24
  yield
25
25
  end
26
26
 
27
- def within_transaction
28
- yield
29
- end
30
-
31
27
  def rollback_transaction
32
28
  raise AdapterMissing if adapter.nil?
33
29
 
@@ -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 RuboCop
6
- # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
7
- # bit of our configuration.
8
- module Inject
9
- PROJECT_ROOT = Pathname.new(__dir__).parent.parent.parent.expand_path.freeze
10
- CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
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
- def self.defaults!
13
- path = CONFIG_DEFAULT.to_s
14
- hash = ConfigLoader.send(:load_yaml_configuration, path)
15
- config = Config.new(hash, path)
16
- puts "configuration from #{path}" if ConfigLoader.debug?
17
- config = ConfigLoader.merge_with_default(config, path)
18
- ConfigLoader.instance_variable_set(:@default_configuration, config)
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
- RuboCop::Inject.defaults!
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#expectations-per-example
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
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
7
  # @example `its`
8
8
  #
9
9
  # # Supports regular `its` call with an attribute/method name,
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
7
  # @internal Support methods for keeping newlines around examples.
8
8
  module LineRangeHelpers
9
9
  include RangeHelp
@@ -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
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
7
  # @internal
8
8
  # Support methods for example metadata.
9
9
  # Examples with similar metadata are grouped.
@@ -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
  # @internal
10
10
  # Node matchers and searchers.
11
11
  module NodeMatchers
@@ -4,14 +4,12 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  class AggregateExamples
7
- def self.inherited(subclass)
8
- superclass.registry.enlist(subclass)
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) do |_event, start, finish, *_args|
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
- # Overrides Minitest.run
41
- def run(*)
42
- if ENV["SAMPLE"]
43
- MinitestSample.sample_examples(ENV["SAMPLE"].to_i)
44
- elsif ENV["SAMPLE_GROUPS"]
45
- MinitestSample.sample_groups(ENV["SAMPLE_GROUPS"].to_i)
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 within_before_all(&block) if within_before_all?
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
- if within_before_all?
90
- within_before_all(&initializer)
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
- define_let_it_be_methods(identifier, **options)
96
- end
101
+ options = default_options.merge(options)
102
+
103
+ before_all(&initializer)
97
104
 
98
- def define_let_it_be_methods(identifier, **modifiers)
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