test-prof 0.11.1 → 0.12.2

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.
@@ -6,7 +6,7 @@ AllCops:
6
6
  - "(?:^|/)spec/"
7
7
 
8
8
  RSpec/AggregateExamples:
9
- Description: Checks if example groups contain two or more aggregatable examples.
9
+ Description: Checks if example group contains two or more aggregatable examples.
10
10
  Enabled: true
11
11
  StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
12
12
  AddAggregateFailuresMetadata: true
@@ -21,8 +21,8 @@ RSpec/AggregateExamples:
21
21
 
22
22
  # TODO: remove this one we hit 1.0
23
23
  RSpec/AggregateFailures:
24
- Description: Checks if example groups contain two or more aggregatable examples.
25
- Enabled: true
24
+ Description: Checks if example group contains two or more aggregatable examples.
25
+ Enabled: false
26
26
  StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
27
27
  AddAggregateFailuresMetadata: true
28
28
  MatchersWithSideEffects:
@@ -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
@@ -8,20 +8,21 @@ module TestProf
8
8
  # store instance variables
9
9
  module Minitest # :nodoc: all
10
10
  class Executor
11
- attr_reader :active
11
+ attr_reader :active, :block, :captured_ivars
12
12
 
13
13
  alias active? active
14
14
 
15
15
  def initialize(&block)
16
16
  @block = block
17
+ @captured_ivars = []
17
18
  end
18
19
 
19
- def activate!(test_class)
20
- return if active?
20
+ def activate!(test_object)
21
+ return restore_ivars(test_object) if active?
21
22
  @active = true
22
- @examples_left = test_class.runnable_methods.size
23
+ @examples_left = test_object.class.runnable_methods.size
23
24
  BeforeAll.begin_transaction do
24
- capture!
25
+ capture!(test_object)
25
26
  end
26
27
  end
27
28
 
@@ -33,16 +34,21 @@ module TestProf
33
34
  BeforeAll.rollback_transaction
34
35
  end
35
36
 
36
- def capture!
37
- instance_eval(&@block)
37
+ def capture!(test_object)
38
+ before_ivars = test_object.instance_variables
39
+
40
+ test_object.instance_eval(&block)
41
+
42
+ (test_object.instance_variables - before_ivars).each do |ivar|
43
+ captured_ivars << [ivar, test_object.instance_variable_get(ivar)]
44
+ end
38
45
  end
39
46
 
40
- def restore_to(test_object)
41
- instance_variables.each do |ivar|
42
- next if ivar == :@block
47
+ def restore_ivars(test_object)
48
+ captured_ivars.each do |(ivar, val)|
43
49
  test_object.instance_variable_set(
44
50
  ivar,
45
- instance_variable_get(ivar)
51
+ val
46
52
  )
47
53
  end
48
54
  end
@@ -62,8 +68,7 @@ module TestProf
62
68
 
63
69
  prepend(Module.new do
64
70
  def setup
65
- self.class.before_all_executor.activate!(self.class)
66
- self.class.before_all_executor.restore_to(self)
71
+ self.class.before_all_executor.activate!(self)
67
72
  super
68
73
  end
69
74
 
@@ -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