test-prof 0.2.5 → 0.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0df1119b6d47cf2641984fe6bcc7387c228353ff
4
- data.tar.gz: d472861344964d895fc964afb099f3d3a35dbfae
3
+ metadata.gz: 1c450b597564326b40288fbee6f6ceb27f6d9ec8
4
+ data.tar.gz: '080d7077ea0713b74a223a421c4a9af6dfc6d8f7'
5
5
  SHA512:
6
- metadata.gz: ccde20ed06a18ff23b85ee1d2b42269214d8ea8dbaf88fda55a78093bd76c99cafef050b3db70e76f98570709b7adfec46d45e9cbb1140233f688c80bc158115
7
- data.tar.gz: 2bda30e78808f8edfa279b88abafa617d416fa6f1bc9f235c883ab106a18d9619c8a5f4db3c412ede368307eade325db0d3559788e2015f2a7aeac6445cdeaed
6
+ metadata.gz: fcbfd05e6c5889d302c309f55a6285e0764838111dcb60af31f0b8ce20cd65619281dc3433c3be35cc9de748a65a3b63ccc58a6bb3506ecc8d490d8652b554e9
7
+ data.tar.gz: 0e5c02a21fe5ebeeb9b457abbb230158a6289fa66e5e694778f7acb0ce0293f50e9ab9c2f97c60b8d9fc78b9738b39e88fa6ed18f6b22f613f198ce2d19bd404
@@ -1,8 +1,17 @@
1
1
  # Change log
2
2
 
3
- ## 0.2.5
3
+ ## 0.3.0
4
4
 
5
- - [#16](https://github.com/palkan/test-prof/pull/16) Support Ruby >= 2.2.0 (was >= 2.3.0). ([@palkan][])
5
+ Features:
6
+
7
+ - [#14](https://github.com/palkan/test-prof/pull/14) RSpecDissect profiler. ([@palkan][])
8
+
9
+ RSpecDissect tracks how much time do you spend in `before` hooks
10
+ and memoization helpers (i.e. `let`) in your tests.
11
+
12
+ - [#13](https://github.com/palkan/test-prof/pull/13) RSpec `let_it_be` method. ([@palkan][])
13
+
14
+ Just like `let`, but persist the result for the whole group (i.e. `let` + `before_all`).
6
15
 
7
16
  ## 0.2.4
8
17
 
data/README.md CHANGED
@@ -24,12 +24,6 @@ Of course, we have some [solutions](#tips-and-tricks) for common performance iss
24
24
 
25
25
  See [Table of Contents](#table-of-contents) for more.
26
26
 
27
- Supported Ruby versions:
28
-
29
- - Ruby (MRI) >= 2.2.0
30
-
31
- - JRuby >= 9.1.0.0
32
-
33
27
  <a href="https://evilmartians.com/">
34
28
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
35
29
 
@@ -69,6 +63,8 @@ Checkout our guides for each specific tool:
69
63
 
70
64
  - [Factory Profiler](https://github.com/palkan/test-prof/tree/master/guides/factory_prof.md)
71
65
 
66
+ - [RSpecDissect Profiler](https://github.com/palkan/test-prof/tree/master/guides/rspec_dissect.md)
67
+
72
68
  - [Rubocop Cops](https://github.com/palkan/test-prof/tree/master/guides/rubocop.md)
73
69
 
74
70
  ## Tips and Tricks (or _Recipes_)
@@ -77,6 +73,8 @@ We also want to share some small code tricks which can help you to improve your
77
73
 
78
74
  - [`before_all` Hook](https://github.com/palkan/test-prof/tree/master/guides/before_all.md)
79
75
 
76
+ - [`let_it_be` Helper](https://github.com/palkan/test-prof/tree/master/guides/let_it_be.md)
77
+
80
78
  - [AnyFixture](https://github.com/palkan/test-prof/tree/master/guides/any_fixture.md)
81
79
 
82
80
  - [FactoryDefault](https://github.com/palkan/test-prof/tree/master/guides/factory_default.md)
@@ -0,0 +1,96 @@
1
+ # Let It Be!
2
+
3
+ Let's bring a little bit of magic and introduce a new way to setup a _shared_ test data.
4
+
5
+ Suppose you have the following setup:
6
+
7
+ ```ruby
8
+ describe BeatleWeightedSearchQuery do
9
+ let!(:paul) { create(:beatle, name: 'Paul') }
10
+ let!(:ringo) { create(:beatle, name: 'Ringo') }
11
+ let!(:george) { create(:beatle, name: 'George') }
12
+ let!(:john) { create(:beatle, name: 'John') }
13
+
14
+ specify { expect(subject.call('joh')).to contain_exactly(john) }
15
+
16
+ # and more examples here
17
+ end
18
+ ```
19
+
20
+ We don't need to re-create the Fab Four for every example, do we?
21
+
22
+ We already have [`before_all`](https://github.com/palkan/test-prof/tree/master/guides/before_all.md) to solve the problem of _repeatable_ data:
23
+
24
+ ```ruby
25
+ describe BeatleWeightedSearchQuery do
26
+ before_all do
27
+ @paul = create(:beatle, name: 'Paul')
28
+ ...
29
+ end
30
+
31
+ specify { expect(subject.call('joh')).to contain_exactly(@john) }
32
+
33
+ ...
34
+ end
35
+ ```
36
+
37
+ That technique works pretty good but requires us to use instance variables and define everything at once. Thus it's not easy to refactor existing tests which use `let/let!` instead.
38
+
39
+ With `let_it_be` you can do the following:
40
+
41
+ ```ruby
42
+ describe BeatleWeightedSearchQuery do
43
+ let_it_be(:paul) { create(:beatle, name: 'Paul') }
44
+ let_it_be(:ringo) { create(:beatle, name: 'Ringo') }
45
+ let_it_be(:george) { create(:beatle, name: 'George') }
46
+ let_it_be(:john) { create(:beatle, name: 'John') }
47
+
48
+ specify { expect(subject.call('joh')).to contain_exactly(john) }
49
+
50
+ # and more examples here
51
+ end
52
+ ```
53
+
54
+ That's it! Just replace `let!` with `let_it_be`. That's equal to the `before_all` approach but requires less refactoring.
55
+
56
+
57
+ ## Instructions
58
+
59
+ In your `spec_helper.rb`:
60
+
61
+ ```ruby
62
+ require "test_prof/recipes/rspec/let_it_be"
63
+ ```
64
+
65
+ In your tests:
66
+
67
+ ```ruby
68
+ describe MySuperDryService do
69
+ let_it_be(:user) { create(:user) }
70
+
71
+ ...
72
+ end
73
+ ```
74
+
75
+ ## Caveats
76
+
77
+ If you modify objects generated within a `let_it_be` block in your examples, you maybe have to re-initiate them.
78
+ We have a built-in support for that:
79
+
80
+
81
+ ```ruby
82
+ # Use reload: true option to reload user object (assuming it's an instance of ActiveRecord)
83
+ # for every example
84
+ let_it_be(:user, reload: true) { create(:user) }
85
+
86
+ # it is almost equal to
87
+ before_all { @user = create(:user) }
88
+ let(:user) { @user.reload }
89
+
90
+ # You can also specify refind: true option to hard-reload the record
91
+ let_it_be(:user, refind: true) { create(:user) }
92
+
93
+ # it is almost equal to
94
+ before_all { @user = create(:user) }
95
+ let(:user) { User.find(@user.id) }
96
+ ```
@@ -0,0 +1,49 @@
1
+ # RSpecDissect
2
+
3
+ Do you know how much time you spend in `before` hooks? Or memoization helpers such as `let`? Usually, the most of the whole test suite time.
4
+
5
+ _RSpecDissect_ provides this kind of information and also shows you the worst example groups. The main purpose of RSpecDissect is to identify these slow groups and refactor them using [`before_all`](https://github.com/palkan/test-prof/tree/master/guides/before_all.md) or [`let_it_be`](https://github.com/palkan/test-prof/tree/master/guides/let_it_be.md) recipes.
6
+
7
+ Example output:
8
+
9
+ ```sh
10
+ [TEST PROF INFO] RSpecDissect enabled
11
+
12
+ Total time: 25:14.870
13
+ Total `before(:each)` time: 14:36.482
14
+ Total `let` time: 19:20.259
15
+
16
+ Top 5 slowest suites (by `before(:each)` time):
17
+
18
+ Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:29.895 of 00:33.706 (327)
19
+ FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:22.117 of 00:43.649 (133)
20
+ ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:21.220 of 00:41.407 (222)
21
+ BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:15.729 of 00:27.893 (50)
22
+ Analytics::Wor...rsion::Summary (./spec/services/analytics/workflow_conversion/summary_spec.rb:3) – 00:15.383 of 00:15.914 (12)
23
+
24
+
25
+ Top 5 slowest suites (by `let` time):
26
+
27
+ FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:38.532 of 00:43.649 (133)
28
+ ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:33.252 of 00:41.407 (222)
29
+ Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:30.320 of 00:33.706 (327)
30
+ BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:25.710 of 00:27.893 (50)
31
+ AvailableSlotsController (./spec/controllers/available_slots_controller_spec.rb:3) – 00:18.481 of 00:23.366 (85)
32
+ ```
33
+
34
+ ## Instructions
35
+
36
+ RSpecDissect can only be used with RSpec (which is clear from the name).
37
+
38
+ To activate RSpecDissect use `RD` environment variable:
39
+
40
+ ```sh
41
+ RD=1 rspec ...
42
+ ```
43
+
44
+ You can also specify the number of top slow groups through `RD_TOP` variable:
45
+
46
+ ```sh
47
+ RD=1 RD_TOP=10 rspec ...
48
+ ```
49
+
@@ -127,3 +127,4 @@ require "test_prof/factory_doctor"
127
127
  require "test_prof/factory_prof"
128
128
  require "test_prof/rspec_stamp"
129
129
  require "test_prof/tag_prof"
130
+ require "test_prof/rspec_dissect"
@@ -39,7 +39,7 @@ module RuboCop
39
39
 
40
40
  def on_block(node)
41
41
  method, _args, body = *node
42
- return unless body && body.begin_type?
42
+ return unless body&.begin_type?
43
43
 
44
44
  _receiver, method_name, _object = *method
45
45
  return unless GROUP_BLOCKS.include?(method_name)
@@ -109,7 +109,7 @@ module RuboCop
109
109
  end
110
110
 
111
111
  def oneliner?(node)
112
- node && node.block_type? &&
112
+ node&.block_type? &&
113
113
  (node.source.lines.size == 1) &&
114
114
  example_node?(node)
115
115
  end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
- using TestProf::StringStripHeredoc
6
-
7
3
  module TestProf::EventProf::CustomEvents
8
4
  module FactoryCreate # :nodoc: all
9
5
  module RunnerPatch
@@ -46,7 +42,7 @@ end
46
42
  TestProf.activate('EVENT_PROF', 'factory.create') do
47
43
  if TestProf.require(
48
44
  'factory_girl',
49
- <<-MSG.strip_heredoc
45
+ <<~MSG
50
46
  Failed to load FactoryGirl.
51
47
 
52
48
  Make sure that "factory_girl" gem is in your Gemfile.
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
- using TestProf::StringStripHeredoc
6
-
7
3
  module TestProf::EventProf::CustomEvents
8
4
  module SidekiqInline # :nodoc: all
9
5
  module ClientPatch
@@ -43,7 +39,7 @@ end
43
39
  TestProf.activate('EVENT_PROF', 'sidekiq.inline') do
44
40
  if TestProf.require(
45
41
  'sidekiq/testing',
46
- <<-MSG.strip_heredoc
42
+ <<~MSG
47
43
  Failed to load Sidekiq.
48
44
 
49
45
  Make sure that "sidekiq" gem is in your Gemfile.
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
- using TestProf::StringStripHeredoc
6
-
7
3
  module TestProf::EventProf::CustomEvents
8
4
  module SidekiqJobs # :nodoc: all
9
5
  module ClientPatch
@@ -30,7 +26,7 @@ end
30
26
  TestProf.activate('EVENT_PROF', 'sidekiq.jobs') do
31
27
  if TestProf.require(
32
28
  'sidekiq/testing',
33
- <<-MSG.strip_heredoc
29
+ <<~MSG
34
30
  Failed to load Sidekiq.
35
31
 
36
32
  Make sure that "sidekiq" gem is in your Gemfile.
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "test_prof/ext/float_duration"
4
4
  require "test_prof/ext/string_truncate"
5
- require "test_prof/ext/string_strip_heredoc"
6
5
 
7
6
  module TestProf
8
7
  module EventProf
@@ -10,7 +9,6 @@ module TestProf
10
9
  include Logging
11
10
  using FloatDuration
12
11
  using StringTruncate
13
- using StringStripHeredoc
14
12
 
15
13
  NOTIFICATIONS = %i[
16
14
  example_group_started
@@ -47,7 +45,7 @@ module TestProf
47
45
  msgs = []
48
46
 
49
47
  msgs <<
50
- <<-MSG.strip_heredoc
48
+ <<~MSG
51
49
  EventProf results for #{@profiler.event}
52
50
 
53
51
  Total time: #{@profiler.total_time.duration}
@@ -62,7 +60,7 @@ module TestProf
62
60
  location = group[:id].metadata[:location]
63
61
 
64
62
  msgs <<
65
- <<-GROUP.strip_heredoc
63
+ <<~GROUP
66
64
  #{description.truncate} (#{location}) – #{group[:time].duration} (#{group[:count]} / #{group[:examples]})
67
65
  GROUP
68
66
  end
@@ -74,7 +72,7 @@ module TestProf
74
72
  description = example[:id].description
75
73
  location = example[:id].metadata[:location]
76
74
  msgs <<
77
- <<-GROUP.strip_heredoc
75
+ <<~GROUP
78
76
  #{description.truncate} (#{location}) – #{example[:time].duration} (#{example[:count]})
79
77
  GROUP
80
78
  end
@@ -16,7 +16,7 @@ module TestProf
16
16
  end
17
17
 
18
18
  def bad?
19
- count > 0 && queries_count.zero?
19
+ count.positive? && queries_count.zero?
20
20
  end
21
21
  end
22
22
 
@@ -111,7 +111,7 @@ module TestProf
111
111
  end
112
112
 
113
113
  def within_factory?
114
- @depth > 0
114
+ @depth.positive?
115
115
  end
116
116
 
117
117
  def ignore?
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/ext/float_duration"
4
- require "test_prof/ext/string_strip_heredoc"
5
4
 
6
5
  module TestProf
7
6
  module FactoryDoctor
8
7
  class RSpecListener # :nodoc:
9
8
  include Logging
10
9
  using FloatDuration
11
- using StringStripHeredoc
12
10
 
13
- SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'.freeze
11
+ SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'
14
12
 
15
13
  NOTIFICATIONS = %i[
16
14
  example_started
@@ -51,7 +49,7 @@ module TestProf
51
49
  msgs = []
52
50
 
53
51
  msgs <<
54
- <<-MSG.strip_heredoc
52
+ <<~MSG
55
53
  FactoryDoctor report
56
54
 
57
55
  Total (potentially) bad examples: #{@count}
@@ -1,19 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
3
  module TestProf::FactoryProf
6
4
  module Printers
7
5
  module Simple # :nodoc: all
8
6
  class << self
9
7
  include TestProf::Logging
10
- using TestProf::StringStripHeredoc
11
8
 
12
9
  def dump(result)
13
10
  msgs = []
14
11
 
15
12
  msgs <<
16
- <<-MSG.strip_heredoc
13
+ <<~MSG
17
14
  Factories usage
18
15
 
19
16
  total top-level name
@@ -12,7 +12,7 @@ RSpec.shared_context "any_fixture:clean", with_clean_fixture: true do
12
12
 
13
13
  def open_transaction?
14
14
  pool = ActiveRecord::Base.connection_pool
15
- pool.active_connection? && pool.connection.open_transactions > 0
15
+ pool.active_connection? && pool.connection.open_transactions.positive?
16
16
  end
17
17
  end
18
18
 
@@ -6,6 +6,10 @@ module TestProf
6
6
  def before_all(&block)
7
7
  raise ArgumentError, "Block is required!" unless block_given?
8
8
 
9
+ return if within_before_all?
10
+
11
+ @__before_all_activated__ = true
12
+
9
13
  before(:all) do
10
14
  ActiveRecord::Base.connection.begin_transaction(joinable: false)
11
15
  instance_eval(&block)
@@ -15,6 +19,10 @@ module TestProf
15
19
  ActiveRecord::Base.connection.rollback_transaction
16
20
  end
17
21
  end
22
+
23
+ def within_before_all?
24
+ instance_variable_defined?(:@__before_all_activated__)
25
+ end
18
26
  end
19
27
  end
20
28
 
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./before_all"
4
+
5
+ module TestProf
6
+ # Just like `let`, but persist the result for the whole group.
7
+ # NOTE: Experimental and magical, for more control use `before_all`.
8
+ module LetItBe
9
+ class << self
10
+ def module_for(group)
11
+ modules.fetch(group) do
12
+ Module.new.tap { |mod| group.prepend(mod) }
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def modules
19
+ @modules ||= {}
20
+ end
21
+ end
22
+ # Use uniq prefix for instance variables to avoid collisions
23
+ # We want to use the power of Ruby's unicode support)
24
+ # And we love cats!)
25
+ PREFIX = "@😸"
26
+
27
+ def let_it_be(identifier, **options, &block)
28
+ initializer = proc do
29
+ instance_variable_set(:"#{PREFIX}#{identifier}", instance_exec(&block))
30
+ end
31
+
32
+ if within_before_all?
33
+ before(:all, &initializer)
34
+ else
35
+ before_all(&initializer)
36
+ end
37
+
38
+ define_let_it_be_methods(identifier, **options)
39
+ end
40
+
41
+ def define_let_it_be_methods(identifier, reload: false, refind: false)
42
+ let_accessor = -> { instance_variable_get(:"#{PREFIX}#{identifier}") }
43
+
44
+ let_accessor = -> { instance_variable_get(:"#{PREFIX}#{identifier}")&.reload } if reload
45
+
46
+ if refind
47
+ let_accessor = lambda do
48
+ record = instance_variable_get(:"#{PREFIX}#{identifier}")
49
+ next unless record.is_a?(::ActiveRecord::Base)
50
+
51
+ record.class.find(record.send(record.class.primary_key))
52
+ end
53
+ end
54
+
55
+ LetItBe.module_for(self).module_eval do
56
+ define_method(identifier) do
57
+ # Trying to detect the context (couldn't find other way so far)
58
+ if @__inspect_output =~ /\(:context\)/
59
+ instance_variable_get(:"#{PREFIX}#{identifier}")
60
+ else
61
+ # Fallback to let definition
62
+ super()
63
+ end
64
+ end
65
+ end
66
+
67
+ let(identifier, &let_accessor)
68
+ end
69
+ end
70
+ end
71
+
72
+ RSpec.configure do |config|
73
+ config.extend TestProf::LetItBe
74
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/logging"
4
+
5
+ module TestProf
6
+ # RSpecDissect tracks how much time do you spend in `before` hooks
7
+ # and memoization helpers (i.e. `let`) in your tests.
8
+ module RSpecDissect
9
+ module ExampleInstrumentation # :nodoc:
10
+ def run_before_example(*)
11
+ RSpecDissect.track(:before) { super }
12
+ end
13
+ end
14
+
15
+ module MemoizedInstrumentation # :nodoc:
16
+ def fetch_or_store(*)
17
+ res = nil
18
+ Thread.current[:_rspec_dissect_memo_depth] ||= 0
19
+ Thread.current[:_rspec_dissect_memo_depth] += 1
20
+ begin
21
+ res = if Thread.current[:_rspec_dissect_memo_depth] == 1
22
+ RSpecDissect.track(:memo) { super }
23
+ else
24
+ super
25
+ end
26
+ ensure
27
+ Thread.current[:_rspec_dissect_memo_depth] -= 1
28
+ end
29
+ res
30
+ end
31
+ end
32
+
33
+ # RSpecDisect configuration
34
+ class Configuration
35
+ attr_accessor :top_count
36
+
37
+ def initialize
38
+ @top_count = (ENV['RD_TOP'] || 5).to_i
39
+ end
40
+ end
41
+
42
+ METRICS = %w[before memo].freeze
43
+
44
+ class << self
45
+ include Logging
46
+
47
+ def config
48
+ @config ||= Configuration.new
49
+ end
50
+
51
+ def configure
52
+ yield config
53
+ end
54
+
55
+ def init
56
+ RSpec::Core::Example.prepend(ExampleInstrumentation)
57
+ RSpec::Core::MemoizedHelpers::ThreadsafeMemoized.prepend(MemoizedInstrumentation)
58
+ RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized.prepend(MemoizedInstrumentation)
59
+
60
+ @data = {}
61
+
62
+ METRICS.each do |type|
63
+ @data["total_#{type}"] = 0.0
64
+ end
65
+
66
+ reset!
67
+
68
+ log :info, "RSpecDissect enabled"
69
+ end
70
+
71
+ def track(type)
72
+ start = TestProf.now
73
+ res = yield
74
+ delta = (TestProf.now - start)
75
+ type = type.to_s
76
+ @data[type] += delta
77
+ @data["total_#{type}"] += delta
78
+ res
79
+ end
80
+
81
+ def reset!
82
+ METRICS.each do |type|
83
+ @data[type.to_s] = 0.0
84
+ end
85
+ end
86
+
87
+ METRICS.each do |type|
88
+ define_method("#{type}_time") do
89
+ @data[type.to_s]
90
+ end
91
+
92
+ define_method("total_#{type}_time") do
93
+ @data["total_#{type}"]
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ require "test_prof/rspec_dissect/rspec" if defined?(RSpec::Core)
101
+
102
+ TestProf.activate('RD') do
103
+ TestProf::RSpecDissect.init
104
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/ext/float_duration"
4
+ require "test_prof/ext/string_truncate"
5
+ require "test_prof/utils/sized_ordered_set"
6
+
7
+ module TestProf
8
+ module RSpecDissect
9
+ class Listener # :nodoc:
10
+ include Logging
11
+ using FloatDuration
12
+ using StringTruncate
13
+
14
+ NOTIFICATIONS = %i[
15
+ example_finished
16
+ example_group_finished
17
+ ].freeze
18
+
19
+ def initialize
20
+ @before_results = Utils::SizedOrderedSet.new(
21
+ top_count, sort_by: :before
22
+ )
23
+ @memo_results = Utils::SizedOrderedSet.new(
24
+ top_count, sort_by: :memo
25
+ )
26
+ @examples_count = 0
27
+ @examples_time = 0.0
28
+ @total_examples_time = 0.0
29
+ end
30
+
31
+ def example_finished(notification)
32
+ @examples_count += 1
33
+ @examples_time += notification.example.execution_result.run_time
34
+ end
35
+
36
+ def example_group_finished(notification)
37
+ return unless notification.group.top_level?
38
+
39
+ data = {}
40
+ data[:total] = @examples_time
41
+ data[:count] = @examples_count
42
+ data[:before] = RSpecDissect.before_time
43
+ data[:memo] = RSpecDissect.memo_time
44
+ data[:desc] = notification.group.top_level_description
45
+ data[:loc] = notification.group.metadata[:location]
46
+
47
+ @before_results << data
48
+ @memo_results << data
49
+
50
+ @total_examples_time += @examples_time
51
+ @examples_count = 0
52
+ @examples_time = 0.0
53
+
54
+ RSpecDissect.reset!
55
+ end
56
+
57
+ def print
58
+ msgs = []
59
+
60
+ msgs <<
61
+ <<~MSG
62
+ RSpecDissect report
63
+
64
+ Total time: #{@total_examples_time.duration}
65
+ Total `before(:each)` time: #{RSpecDissect.total_before_time.duration}
66
+ Total `let` time: #{RSpecDissect.total_memo_time.duration}
67
+
68
+ MSG
69
+
70
+ msgs <<
71
+ <<~MSG
72
+ Top #{top_count} slowest suites (by `before(:each)` time):
73
+
74
+ MSG
75
+
76
+ @before_results.each do |group|
77
+ msgs <<
78
+ <<~GROUP
79
+ #{group[:desc].truncate} (#{group[:loc]}) – #{group[:before].duration} of #{group[:total].duration} (#{group[:count]})
80
+ GROUP
81
+ end
82
+
83
+ msgs <<
84
+ <<~MSG
85
+ Top #{top_count} slowest suites (by `let` time):
86
+
87
+ MSG
88
+
89
+ @memo_results.each do |group|
90
+ msgs <<
91
+ <<~GROUP
92
+ #{group[:desc].truncate} (#{group[:loc]}) – #{group[:memo].duration} of #{group[:total].duration} (#{group[:count]})
93
+ GROUP
94
+ end
95
+
96
+ log :info, msgs.join
97
+ end
98
+
99
+ private
100
+
101
+ def top_count
102
+ RSpecDissect.config.top_count
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Register RSpecDissect listener
109
+ TestProf.activate('RD') do
110
+ RSpec.configure do |config|
111
+ listener = TestProf::RSpecDissect::Listener.new
112
+
113
+ config.before(:suite) do
114
+ config.reporter.register_listener(
115
+ listener, *TestProf::RSpecDissect::Listener::NOTIFICATIONS
116
+ )
117
+ end
118
+
119
+ config.after(:suite) { listener.print }
120
+ end
121
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "ripper"
4
4
 
5
+ # rubocop: disable Metrics/CyclomaticComplexity
6
+
5
7
  module TestProf
6
8
  module RSpecStamp
7
9
  # Parse examples headers
@@ -22,13 +24,12 @@ module TestProf
22
24
  end
23
25
 
24
26
  def remove_tag(tag)
25
- @tags.delete(tag) if @tags
26
- @htags.delete_if { |(k, _v)| k == tag } if @htags
27
+ @tags&.delete(tag)
28
+ @htags&.delete_if { |(k, _v)| k == tag }
27
29
  end
28
30
  end
29
31
 
30
32
  class << self
31
- # rubocop: disable Metrics/CyclomaticComplexity
32
33
  def parse(code)
33
34
  sexp = Ripper.sexp(code)
34
35
  return unless sexp
@@ -76,7 +77,6 @@ module TestProf
76
77
 
77
78
  res
78
79
  end
79
- # rubocop: enable Metrics/CyclomaticComplexity
80
80
 
81
81
  private
82
82
 
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
3
  module TestProf
6
4
  module RSpecStamp
7
5
  class RSpecListener # :nodoc:
8
6
  include Logging
9
- using StringStripHeredoc
10
7
 
11
8
  NOTIFICATIONS = %i[
12
9
  example_failed
@@ -37,7 +34,7 @@ module TestProf
37
34
  msgs = []
38
35
 
39
36
  msgs <<
40
- <<-MSG.strip_heredoc
37
+ <<~MSG
41
38
  RSpec Stamp results
42
39
 
43
40
  Total patches: #{@total}
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
3
  module TestProf
6
4
  # RubyProf wrapper.
7
5
  #
@@ -121,7 +119,6 @@ module TestProf
121
119
 
122
120
  class << self
123
121
  include Logging
124
- using StringStripHeredoc
125
122
 
126
123
  def config
127
124
  @config ||= Configuration.new
@@ -175,7 +172,7 @@ module TestProf
175
172
  ENV["RUBY_PROF_MEASURE_MODE"] = config.mode.to_s
176
173
  @initialized = TestProf.require(
177
174
  'ruby-prof',
178
- <<-MSG.strip_heredoc
175
+ <<~MSG
179
176
  Please, install 'ruby-prof' first:
180
177
  # Gemfile
181
178
  gem 'ruby-prof', '>= 0.16.0', require: false
@@ -187,7 +184,7 @@ module TestProf
187
184
  if Utils.verify_gem_version('ruby-prof', at_least: '0.16.0')
188
185
  true
189
186
  else
190
- log :error, <<-MGS.strip_heredoc
187
+ log :error, <<~MGS
191
188
  Please, upgrade 'ruby-prof' to version >= 0.16.0.
192
189
  MGS
193
190
  false
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/string_strip_heredoc"
4
-
5
3
  module TestProf
6
4
  # StackProf wrapper.
7
5
  #
@@ -34,7 +32,6 @@ module TestProf
34
32
 
35
33
  class << self
36
34
  include Logging
37
- using StringStripHeredoc
38
35
 
39
36
  def config
40
37
  @config ||= Configuration.new
@@ -91,7 +88,7 @@ module TestProf
91
88
 
92
89
  html_path = path.gsub(/\.dump$/, '.html')
93
90
 
94
- log :info, <<-MSG.strip_heredoc
91
+ log :info, <<~MSG
95
92
  Run the following command to generate a flame graph report:
96
93
 
97
94
  stackprof --flamegraph #{path} > #{html_path} && stackprof --flamegraph-viewer=#{html_path}
@@ -114,7 +111,7 @@ module TestProf
114
111
  return @initialized if instance_variable_defined?(:@initialized)
115
112
  @initialized = TestProf.require(
116
113
  'stackprof',
117
- <<-MSG.strip_heredoc
114
+ <<~MSG
118
115
  Please, install 'stackprof' first:
119
116
  # Gemfile
120
117
  gem 'stackprof', '>= 0.2.9', require: false
@@ -126,9 +123,9 @@ module TestProf
126
123
  if Utils.verify_gem_version('stackprof', at_least: '0.2.9')
127
124
  true
128
125
  else
129
- log :error, <<-MSG.strip_heredoc
126
+ log :error, <<~MGS
130
127
  Please, upgrade 'stackprof' to version >= 0.2.9.
131
- MSG
128
+ MGS
132
129
  false
133
130
  end
134
131
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/ext/float_duration"
4
- require "test_prof/ext/string_strip_heredoc"
5
4
 
6
5
  module TestProf
7
6
  module TagProf
8
7
  class RSpecListener # :nodoc:
9
8
  include Logging
10
9
  using FloatDuration
11
- using StringStripHeredoc
12
10
 
13
11
  NOTIFICATIONS = %i[
14
12
  example_started
@@ -39,7 +37,7 @@ module TestProf
39
37
  msgs = []
40
38
 
41
39
  msgs <<
42
- <<-MSG.strip_heredoc
40
+ <<~MSG
43
41
  TagProf report for #{@tag}
44
42
  MSG
45
43
 
@@ -4,11 +4,6 @@ module TestProf
4
4
  module Utils
5
5
  # Ordered set with capacity
6
6
  class SizedOrderedSet
7
- unless [].respond_to?(:bsearch_index)
8
- require "test_prof/ext/array_bsearch_index"
9
- using ArrayBSearchIndex
10
- end
11
-
12
7
  include Enumerable
13
8
 
14
9
  def initialize(max_size, sort_by: nil)
@@ -42,7 +37,11 @@ module TestProf
42
37
  end
43
38
 
44
39
  def each
45
- data.each
40
+ if block_given?
41
+ data.each(&Proc.new)
42
+ else
43
+ data.each
44
+ end
46
45
  end
47
46
 
48
47
  def size
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "0.2.5".freeze
4
+ VERSION = "0.3.0.beta"
5
5
  end
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: 0.2.5
4
+ version: 0.3.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-30 00:00:00.000000000 Z
11
+ date: 2017-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.49'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  description: "\n Ruby applications tests profiling tools.\n\n Contains tools
84
98
  to anylyze factories usage, integrate with Ruby profilers,\n profile your examples
85
99
  using ActiveSupport notifications (if any) and\n statically analyze your code
@@ -108,6 +122,8 @@ files:
108
122
  - guides/factory_default.md
109
123
  - guides/factory_doctor.md
110
124
  - guides/factory_prof.md
125
+ - guides/let_it_be.md
126
+ - guides/rspec_dissect.md
111
127
  - guides/rspec_stamp.md
112
128
  - guides/rubocop.md
113
129
  - guides/ruby_prof.md
@@ -126,9 +142,7 @@ files:
126
142
  - lib/test_prof/event_prof/instrumentations/active_support.rb
127
143
  - lib/test_prof/event_prof/minitest.rb
128
144
  - lib/test_prof/event_prof/rspec.rb
129
- - lib/test_prof/ext/array_bsearch_index.rb
130
145
  - lib/test_prof/ext/float_duration.rb
131
- - lib/test_prof/ext/string_strip_heredoc.rb
132
146
  - lib/test_prof/ext/string_truncate.rb
133
147
  - lib/test_prof/factory_default.rb
134
148
  - lib/test_prof/factory_default/factory_girl_patch.rb
@@ -145,7 +159,10 @@ files:
145
159
  - lib/test_prof/recipes/rspec/any_fixture.rb
146
160
  - lib/test_prof/recipes/rspec/before_all.rb
147
161
  - lib/test_prof/recipes/rspec/factory_default.rb
162
+ - lib/test_prof/recipes/rspec/let_it_be.rb
148
163
  - lib/test_prof/recipes/rspec/sample.rb
164
+ - lib/test_prof/rspec_dissect.rb
165
+ - lib/test_prof/rspec_dissect/rspec.rb
149
166
  - lib/test_prof/rspec_stamp.rb
150
167
  - lib/test_prof/rspec_stamp/parser.rb
151
168
  - lib/test_prof/rspec_stamp/rspec.rb
@@ -171,15 +188,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
171
188
  requirements:
172
189
  - - ">="
173
190
  - !ruby/object:Gem::Version
174
- version: 2.2.0
191
+ version: 2.3.0
175
192
  required_rubygems_version: !ruby/object:Gem::Requirement
176
193
  requirements:
177
- - - ">="
194
+ - - ">"
178
195
  - !ruby/object:Gem::Version
179
- version: '0'
196
+ version: 1.3.1
180
197
  requirements: []
181
198
  rubyforge_project:
182
- rubygems_version: 2.6.13
199
+ rubygems_version: 2.6.11
183
200
  signing_key:
184
201
  specification_version: 4
185
202
  summary: Ruby applications tests profiling tools
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TestProf
4
- # Ruby 2.3 #bsearch_index method (for usage with older Rubies)
5
- # Straighforward and non-optimal implementation,
6
- # just for compatiblity
7
- module ArrayBSearchIndex
8
- refine Array do
9
- def bsearch_index(&block)
10
- el = bsearch(&block)
11
- index(el)
12
- end
13
- end
14
- end
15
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TestProf
4
- # Add #strip_heredoc method to use instead of
5
- # squiggly docs (to support older Rubies)
6
- module StringStripHeredoc
7
- refine String do
8
- def strip_heredoc
9
- min = scan(/^[ \t]*(?=\S)/).min
10
- indent = min ? min.size : 0
11
- gsub(/^[ \t]{#{indent}}/, '')
12
- end
13
- end
14
- end
15
- end