test-prof 0.11.1 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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