test-prof 0.2.5 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0df1119b6d47cf2641984fe6bcc7387c228353ff
4
- data.tar.gz: d472861344964d895fc964afb099f3d3a35dbfae
3
+ metadata.gz: eb103d86ccc7a513de8b30ae8ddf536c9bbc6255
4
+ data.tar.gz: f0fe1c206faab5761b4419ccbf8745f4060d3e82
5
5
  SHA512:
6
- metadata.gz: ccde20ed06a18ff23b85ee1d2b42269214d8ea8dbaf88fda55a78093bd76c99cafef050b3db70e76f98570709b7adfec46d45e9cbb1140233f688c80bc158115
7
- data.tar.gz: 2bda30e78808f8edfa279b88abafa617d416fa6f1bc9f235c883ab106a18d9619c8a5f4db3c412ede368307eade325db0d3559788e2015f2a7aeac6445cdeaed
6
+ metadata.gz: f6a4881cfc954438fa4184c2cd9be1784d10bfcd6d92931ea5132c8422aa08866b77e60ec91066ddd6afb620c524e99b4584818d158f396176f9649ef3a0e038
7
+ data.tar.gz: a699bd6e308b370aa4e53447025551a5b2f30249eb893356d30238eaed4dc135bb69335c780ea9d0b306ce569bca29117494ab91b6a70bbb7a22f714a65f827f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Change log
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Features:
6
+
7
+ - Combine RSpecStamp with FactoryDoctor. ([@palkan][])
8
+
9
+ Automatically mark _bad_ examples with custom tags.
10
+
11
+ - [#17](https://github.com/palkan/test-prof/pull/17) Combine RSpecStamp with EventProf and RSpecDissect. ([@palkan][])
12
+
13
+ It is possible now to automatically mark _slow_ examples and groups with custom tags. For example:
14
+
15
+ ```sh
16
+ EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ...
17
+ ```
18
+
19
+ After running the command above the top 5 slowest example groups would be marked with `slow: :sql` tag.
20
+
21
+ - [#14](https://github.com/palkan/test-prof/pull/14) RSpecDissect profiler. ([@palkan][])
22
+
23
+ RSpecDissect tracks how much time do you spend in `before` hooks
24
+ and memoization helpers (i.e. `let`) in your tests.
25
+
26
+ - [#13](https://github.com/palkan/test-prof/pull/13) RSpec `let_it_be` method. ([@palkan][])
27
+
28
+ Just like `let`, but persist the result for the whole group (i.e. `let` + `before_all`).
29
+
30
+ ### Improvements:
31
+
32
+ - Add ability to specify RubyProf report through `TEST_RUBY_PROF` env variable. ([@palkan][])
33
+
34
+ - Add ability to specify StackProf raw mode through `TEST_STACK_PROF` env variable. ([@palkan][])
35
+
36
+ ### Changes
37
+
38
+ - Use RubyProf `FlatPrinter` by default (was `CallStackPrinter`). ([@palkan][])
39
+
3
40
  ## 0.2.5
4
41
 
5
42
  - [#16](https://github.com/palkan/test-prof/pull/16) Support Ruby >= 2.2.0 (was >= 2.3.0). ([@palkan][])
data/README.md CHANGED
@@ -10,7 +10,7 @@ Simply speaking, slow tests waste your time making you less productive.
10
10
 
11
11
  TestProf toolbox aims to help you identify bottlenecks in your test suite. It contains:
12
12
 
13
- - Plug'n'Play integrations for general Ruby profilers ([`ruby-prof`](https://github.com/ruby-prof), [`stackprof`](https://github.com/tmm1/stackprof))
13
+ - Plug'n'Play integrations for general Ruby profilers ([`ruby-prof`](https://github.com/ruby-prof/ruby-prof), [`stackprof`](https://github.com/tmm1/stackprof))
14
14
 
15
15
  - Factories usage analyzers and profilers
16
16
 
@@ -20,7 +20,7 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
20
20
 
21
21
  - etc.
22
22
 
23
- Of course, we have some [solutions](#tips-and-tricks) for common performance issues too, bundled into the gem.
23
+ Of course, we have some [solutions](#tips-and-tricks-or-recipes) for common performance issues too, bundled into the gem.
24
24
 
25
25
  See [Table of Contents](#table-of-contents) for more.
26
26
 
@@ -69,6 +69,8 @@ Checkout our guides for each specific tool:
69
69
 
70
70
  - [Factory Profiler](https://github.com/palkan/test-prof/tree/master/guides/factory_prof.md)
71
71
 
72
+ - [RSpecDissect Profiler](https://github.com/palkan/test-prof/tree/master/guides/rspec_dissect.md)
73
+
72
74
  - [Rubocop Cops](https://github.com/palkan/test-prof/tree/master/guides/rubocop.md)
73
75
 
74
76
  ## Tips and Tricks (or _Recipes_)
@@ -77,6 +79,8 @@ We also want to share some small code tricks which can help you to improve your
77
79
 
78
80
  - [`before_all` Hook](https://github.com/palkan/test-prof/tree/master/guides/before_all.md)
79
81
 
82
+ - [`let_it_be` Helper](https://github.com/palkan/test-prof/tree/master/guides/let_it_be.md)
83
+
80
84
  - [AnyFixture](https://github.com/palkan/test-prof/tree/master/guides/any_fixture.md)
81
85
 
82
86
  - [FactoryDefault](https://github.com/palkan/test-prof/tree/master/guides/factory_default.md)
data/guides/event_prof.md CHANGED
@@ -66,6 +66,16 @@ EVENT_PROF_RANK=count EVENT_PROF='instantiation.active_record' be rspec
66
66
 
67
67
  See [event_prof.rb](https://github.com/palkan/test-prof/tree/master/lib/test_prof/event_prof.rb) for all available configuration options and their usage.
68
68
 
69
+ ## Using with RSpecStamp
70
+
71
+ EventProf can be used with [RSpec Stamp](https://github.com/palkan/test-prof/tree/master/guides/rspec_stamp.md) to automatically mark _slow_ examples with custom tags. For example:
72
+
73
+ ```sh
74
+ EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ...
75
+ ```
76
+
77
+ After running the command above the slowest example groups (and examples if configured) would be marked with the `slow: :sql` tag.
78
+
69
79
  ## Custom Instrumentation
70
80
 
71
81
  To use EventProf with your instrumentation engine just complete the two following steps:
@@ -62,3 +62,13 @@ To activate FactoryDoctor use `FDOC` environment variable:
62
62
  ```sh
63
63
  FDOC=1 rspec ...
64
64
  ```
65
+
66
+ ## Using with RSpecStamp
67
+
68
+ FactoryDoctor can be used with [RSpec Stamp](https://github.com/palkan/test-prof/tree/master/guides/rspec_stamp.md) to automatically mark _bad_ examples with custom tags. For example:
69
+
70
+ ```sh
71
+ FDOC=1 FDOC_STAMP="fdoc:consider" rspec ...
72
+ ```
73
+
74
+ After running the command above all _potentially_ bad examples would be marked with the `fdoc: :consider` tag.
@@ -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,58 @@
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_PROF` environment variable:
39
+
40
+ ```sh
41
+ RD_PROF=1 rspec ...
42
+ ```
43
+
44
+ You can also specify the number of top slow groups through `RD_PROF_TOP` variable:
45
+
46
+ ```sh
47
+ RD_PROF=1 RD_PROF_TOP=10 rspec ...
48
+ ```
49
+
50
+ ## Using with RSpecStamp
51
+
52
+ RSpecDissect can be used with [RSpec Stamp](https://github.com/palkan/test-prof/tree/master/guides/rspec_stamp.md) to automatically mark _slow_ examples with custom tags. For example:
53
+
54
+ ```sh
55
+ RD_PROF=1 RD_PROF_STAMP="slow" rspec ...
56
+ ```
57
+
58
+ After running the command above the slowest example groups would be marked with the `:slow` tag.
data/guides/ruby_prof.md CHANGED
@@ -42,21 +42,21 @@ end
42
42
 
43
43
  The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers).
44
44
 
45
- You can specify a printer through environment variable `TEST_RUBY_PROF_PRINTER`:
45
+ You can specify a printer through environment variable `TEST_RUBY_PROF`:
46
46
 
47
47
  ```sh
48
- TEST_RUBY_PROF_PRINTER=flat bundle exec rake test
48
+ TEST_RUBY_PROF=call_stack bundle exec rake test
49
49
  ```
50
50
 
51
51
  Or in your code:
52
52
 
53
53
  ```ruby
54
54
  TestProf::RubyProf.configure do |config|
55
- config.printer = :flat
55
+ config.printer = :call_stack
56
56
  end
57
57
  ```
58
58
 
59
- By default, we use `CallStackPrinter`.
59
+ By default, we use `FlatPrinter`.
60
60
 
61
61
  Also, you can specify RubyProf mode (`wall`, `cpu`, etc) through `TEST_RUBY_PROF_MODE` env variable.
62
62
 
data/guides/stack_prof.md CHANGED
@@ -42,6 +42,6 @@ end
42
42
 
43
43
  You can change StackProf mode (which is `wall` by default) through `TEST_STACK_PROF_MODE` env variable.
44
44
 
45
- If you want to generate flame graphs you should collect _raw_ data. Turn _raw_ collection on by passing `TEST_STACK_PROF_RAW=1`.
45
+ If you want to generate flame graphs you should collect _raw_ data. Turn _raw_ collection on by passing `TEST_STACK_PROF=raw`.
46
46
 
47
47
  See [stack_prof.rb](https://github.com/palkan/test-prof/tree/master/lib/test_prof/stack_prof.rb) for all available configuration options and their usage.
@@ -81,6 +81,41 @@ module TestProf
81
81
  end
82
82
 
83
83
  log :info, msgs.join
84
+
85
+ stamp! if EventProf.config.stamp?
86
+ end
87
+
88
+ def stamp!
89
+ result = @profiler.results
90
+
91
+ stamper = RSpecStamp::Stamper.new
92
+
93
+ examples = Hash.new { |h, k| h[k] = [] }
94
+
95
+ (result[:groups].to_a + result.fetch(:examples, []).to_a)
96
+ .map { |obj| obj[:id].metadata[:location] }.each do |location|
97
+ file, line = location.split(":")
98
+ examples[file] << line.to_i
99
+ end
100
+
101
+ examples.each do |file, lines|
102
+ stamper.stamp_file(file, lines.uniq)
103
+ end
104
+
105
+ msgs = []
106
+
107
+ msgs <<
108
+ <<-MSG.strip_heredoc
109
+ RSpec Stamp results
110
+
111
+ Total patches: #{stamper.total}
112
+ Total files: #{examples.keys.size}
113
+
114
+ Failed patches: #{stamper.failed}
115
+ Ignored files: #{stamper.ignored}
116
+ MSG
117
+
118
+ log :info, msgs.join
84
119
  end
85
120
  end
86
121
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/rspec_stamp"
3
4
  require "test_prof/event_prof/instrumentations/active_support"
4
5
  require "test_prof/utils/sized_ordered_set"
5
6
 
@@ -39,6 +40,13 @@ module TestProf
39
40
  @top_count = (ENV['EVENT_PROF_TOP'] || 5).to_i
40
41
  @per_example = ENV['EVENT_PROF_EXAMPLES'] == '1'
41
42
  @rank_by = (ENV['EVENT_PROF_RANK'] || :time).to_sym
43
+ @stamp = ENV['EVENT_PROF_STAMP']
44
+
45
+ RSpecStamp.config.tags = @stamp if stamp?
46
+ end
47
+
48
+ def stamp?
49
+ !@stamp.nil?
42
50
  end
43
51
 
44
52
  def per_example?
@@ -76,7 +84,7 @@ module TestProf
76
84
  class Profiler # :nodoc:
77
85
  include TestProf::Logging
78
86
 
79
- attr_reader :event, :top_count, :rank_by, :total_count, :total_time
87
+ attr_reader :event, :total_count, :total_time
80
88
 
81
89
  def initialize(event:, instrumenter:)
82
90
  @event = event
@@ -70,6 +70,40 @@ module TestProf
70
70
  end
71
71
 
72
72
  log :info, msgs.join
73
+
74
+ stamp! if FactoryDoctor.stamp?
75
+ end
76
+
77
+ def stamp!
78
+ stamper = RSpecStamp::Stamper.new
79
+
80
+ examples = Hash.new { |h, k| h[k] = [] }
81
+
82
+ @example_groups.each_value do |bad_examples|
83
+ bad_examples.each do |example|
84
+ file, line = example.metadata[:location].split(":")
85
+ examples[file] << line.to_i
86
+ end
87
+ end
88
+
89
+ examples.each do |file, lines|
90
+ stamper.stamp_file(file, lines.uniq)
91
+ end
92
+
93
+ msgs = []
94
+
95
+ msgs <<
96
+ <<-MSG.strip_heredoc
97
+ RSpec Stamp results
98
+
99
+ Total patches: #{stamper.total}
100
+ Total files: #{examples.keys.size}
101
+
102
+ Failed patches: #{stamper.failed}
103
+ Ignored files: #{stamper.ignored}
104
+ MSG
105
+
106
+ log :info, msgs.join
73
107
  end
74
108
 
75
109
  private
@@ -54,6 +54,14 @@ module TestProf
54
54
  defined?(::FactoryGirl)
55
55
 
56
56
  subscribe!
57
+
58
+ @stamp = ENV['FDOC_STAMP']
59
+
60
+ RSpecStamp.config.tags = @stamp if stamp?
61
+ end
62
+
63
+ def stamp?
64
+ !@stamp.nil?
57
65
  end
58
66
 
59
67
  def start
@@ -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,80 @@
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 = RUBY_ENGINE == 'jruby' ? "@__jruby_is_not_cat_friendly__".freeze : "@😸".freeze
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
+ if reload
45
+ let_accessor = lambda do
46
+ record = instance_variable_get(:"#{PREFIX}#{identifier}")
47
+ next unless record.is_a?(::ActiveRecord::Base)
48
+ record.reload
49
+ end
50
+ end
51
+
52
+ if refind
53
+ let_accessor = lambda do
54
+ record = instance_variable_get(:"#{PREFIX}#{identifier}")
55
+ next unless record.is_a?(::ActiveRecord::Base)
56
+
57
+ record.class.find(record.send(record.class.primary_key))
58
+ end
59
+ end
60
+
61
+ LetItBe.module_for(self).module_eval do
62
+ define_method(identifier) do
63
+ # Trying to detect the context (couldn't find other way so far)
64
+ if @__inspect_output =~ /\(:context\)/
65
+ instance_variable_get(:"#{PREFIX}#{identifier}")
66
+ else
67
+ # Fallback to let definition
68
+ super()
69
+ end
70
+ end
71
+ end
72
+
73
+ let(identifier, &let_accessor)
74
+ end
75
+ end
76
+ end
77
+
78
+ RSpec.configure do |config|
79
+ config.extend TestProf::LetItBe
80
+ end
@@ -0,0 +1,157 @@
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
+ require "test_prof/ext/string_strip_heredoc"
7
+
8
+ module TestProf
9
+ module RSpecDissect
10
+ class Listener # :nodoc:
11
+ include Logging
12
+ using FloatDuration
13
+ using StringTruncate
14
+ using StringStripHeredoc
15
+
16
+ NOTIFICATIONS = %i[
17
+ example_finished
18
+ example_group_finished
19
+ ].freeze
20
+
21
+ def initialize
22
+ @before_results = Utils::SizedOrderedSet.new(
23
+ top_count, sort_by: :before
24
+ )
25
+ @memo_results = Utils::SizedOrderedSet.new(
26
+ top_count, sort_by: :memo
27
+ )
28
+ @examples_count = 0
29
+ @examples_time = 0.0
30
+ @total_examples_time = 0.0
31
+ end
32
+
33
+ def example_finished(notification)
34
+ @examples_count += 1
35
+ @examples_time += notification.example.execution_result.run_time
36
+ end
37
+
38
+ def example_group_finished(notification)
39
+ return unless notification.group.top_level?
40
+
41
+ data = {}
42
+ data[:total] = @examples_time
43
+ data[:count] = @examples_count
44
+ data[:before] = RSpecDissect.before_time
45
+ data[:memo] = RSpecDissect.memo_time
46
+ data[:desc] = notification.group.top_level_description
47
+ data[:loc] = notification.group.metadata[:location]
48
+
49
+ @before_results << data
50
+ @memo_results << data
51
+
52
+ @total_examples_time += @examples_time
53
+ @examples_count = 0
54
+ @examples_time = 0.0
55
+
56
+ RSpecDissect.reset!
57
+ end
58
+
59
+ def print
60
+ msgs = []
61
+
62
+ msgs <<
63
+ <<-MSG.strip_heredoc
64
+ RSpecDissect report
65
+
66
+ Total time: #{@total_examples_time.duration}
67
+ Total `before(:each)` time: #{RSpecDissect.total_before_time.duration}
68
+ Total `let` time: #{RSpecDissect.total_memo_time.duration}
69
+
70
+ MSG
71
+
72
+ msgs <<
73
+ <<-MSG.strip_heredoc
74
+ Top #{top_count} slowest suites (by `before(:each)` time):
75
+
76
+ MSG
77
+
78
+ @before_results.each do |group|
79
+ msgs <<
80
+ <<-GROUP.strip_heredoc
81
+ #{group[:desc].truncate} (#{group[:loc]}) – #{group[:before].duration} of #{group[:total].duration} (#{group[:count]})
82
+ GROUP
83
+ end
84
+
85
+ msgs <<
86
+ <<-MSG.strip_heredoc
87
+
88
+ Top #{top_count} slowest suites (by `let` time):
89
+
90
+ MSG
91
+
92
+ @memo_results.each do |group|
93
+ msgs <<
94
+ <<-GROUP.strip_heredoc
95
+ #{group[:desc].truncate} (#{group[:loc]}) – #{group[:memo].duration} of #{group[:total].duration} (#{group[:count]})
96
+ GROUP
97
+ end
98
+
99
+ log :info, msgs.join
100
+
101
+ stamp! if RSpecDissect.config.stamp?
102
+ end
103
+
104
+ def stamp!
105
+ stamper = RSpecStamp::Stamper.new
106
+
107
+ examples = Hash.new { |h, k| h[k] = [] }
108
+
109
+ (@before_results.to_a + @memo_results.to_a)
110
+ .map { |obj| obj[:loc] }.each do |location|
111
+ file, line = location.split(":")
112
+ examples[file] << line.to_i
113
+ end
114
+
115
+ examples.each do |file, lines|
116
+ stamper.stamp_file(file, lines.uniq)
117
+ end
118
+
119
+ msgs = []
120
+
121
+ msgs <<
122
+ <<-MSG.strip_heredoc
123
+ RSpec Stamp results
124
+
125
+ Total patches: #{stamper.total}
126
+ Total files: #{examples.keys.size}
127
+
128
+ Failed patches: #{stamper.failed}
129
+ Ignored files: #{stamper.ignored}
130
+ MSG
131
+
132
+ log :info, msgs.join
133
+ end
134
+
135
+ private
136
+
137
+ def top_count
138
+ RSpecDissect.config.top_count
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # Register RSpecDissect listener
145
+ TestProf.activate('RD_PROF') do
146
+ RSpec.configure do |config|
147
+ listener = TestProf::RSpecDissect::Listener.new
148
+
149
+ config.before(:suite) do
150
+ config.reporter.register_listener(
151
+ listener, *TestProf::RSpecDissect::Listener::NOTIFICATIONS
152
+ )
153
+ end
154
+
155
+ config.after(:suite) { listener.print }
156
+ end
157
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/rspec_stamp"
4
+ require "test_prof/logging"
5
+
6
+ module TestProf
7
+ # RSpecDissect tracks how much time do you spend in `before` hooks
8
+ # and memoization helpers (i.e. `let`) in your tests.
9
+ module RSpecDissect
10
+ module ExampleInstrumentation # :nodoc:
11
+ def run_before_example(*)
12
+ RSpecDissect.track(:before) { super }
13
+ end
14
+ end
15
+
16
+ module MemoizedInstrumentation # :nodoc:
17
+ def fetch_or_store(*)
18
+ res = nil
19
+ Thread.current[:_rspec_dissect_memo_depth] ||= 0
20
+ Thread.current[:_rspec_dissect_memo_depth] += 1
21
+ begin
22
+ res = if Thread.current[:_rspec_dissect_memo_depth] == 1
23
+ RSpecDissect.track(:memo) { super }
24
+ else
25
+ super
26
+ end
27
+ ensure
28
+ Thread.current[:_rspec_dissect_memo_depth] -= 1
29
+ end
30
+ res
31
+ end
32
+ end
33
+
34
+ # RSpecDisect configuration
35
+ class Configuration
36
+ attr_accessor :top_count
37
+
38
+ def initialize
39
+ @top_count = (ENV['RD_PROF_TOP'] || 5).to_i
40
+ @stamp = ENV['RD_PROF_STAMP']
41
+
42
+ RSpecStamp.config.tags = @stamp if stamp?
43
+ end
44
+
45
+ def stamp?
46
+ !@stamp.nil?
47
+ end
48
+ end
49
+
50
+ METRICS = %w[before memo].freeze
51
+
52
+ class << self
53
+ include Logging
54
+
55
+ def config
56
+ @config ||= Configuration.new
57
+ end
58
+
59
+ def configure
60
+ yield config
61
+ end
62
+
63
+ def init
64
+ RSpec::Core::Example.prepend(ExampleInstrumentation)
65
+ RSpec::Core::MemoizedHelpers::ThreadsafeMemoized.prepend(MemoizedInstrumentation)
66
+ RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized.prepend(MemoizedInstrumentation)
67
+
68
+ @data = {}
69
+
70
+ METRICS.each do |type|
71
+ @data["total_#{type}"] = 0.0
72
+ end
73
+
74
+ reset!
75
+
76
+ log :info, "RSpecDissect enabled"
77
+ end
78
+
79
+ def track(type)
80
+ start = TestProf.now
81
+ res = yield
82
+ delta = (TestProf.now - start)
83
+ type = type.to_s
84
+ @data[type] += delta
85
+ @data["total_#{type}"] += delta
86
+ res
87
+ end
88
+
89
+ def reset!
90
+ METRICS.each do |type|
91
+ @data[type.to_s] = 0.0
92
+ end
93
+ end
94
+
95
+ METRICS.each do |type|
96
+ define_method("#{type}_time") do
97
+ @data[type.to_s]
98
+ end
99
+
100
+ define_method("total_#{type}_time") do
101
+ @data["total_#{type}"]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ require "test_prof/rspec_dissect/rspec" if defined?(RSpec::Core)
109
+
110
+ TestProf.activate('RD_PROF') do
111
+ TestProf::RSpecDissect.init
112
+ end
@@ -8,7 +8,7 @@ module TestProf
8
8
  module Parser
9
9
  # Contains the result of parsing
10
10
  class Result
11
- attr_accessor :fname, :desc
11
+ attr_accessor :fname, :desc, :desc_const
12
12
  attr_reader :tags, :htags
13
13
 
14
14
  def add_tag(v)
@@ -29,6 +29,7 @@ module TestProf
29
29
 
30
30
  class << self
31
31
  # rubocop: disable Metrics/CyclomaticComplexity
32
+ # rubocop: disable Metrics/PerceivedComplexity
32
33
  def parse(code)
33
34
  sexp = Ripper.sexp(code)
34
35
  return unless sexp
@@ -57,11 +58,17 @@ module TestProf
57
58
  res = Result.new
58
59
 
59
60
  fcall = sexp[1][0][1]
60
- fcall = fcall[1] if fcall.first == :fcall
61
- res.fname = fcall[1]
62
-
63
61
  args_block = sexp[1][0][2]
64
62
 
63
+ if fcall.first == :fcall
64
+ fcall = fcall[1]
65
+ elsif fcall.first == :var_ref
66
+ res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
67
+ args_block = sexp[1][0][4]
68
+ end
69
+
70
+ res.fname ||= fcall[1]
71
+
65
72
  return res if args_block.nil?
66
73
 
67
74
  args_block = args_block[1] if args_block.first == :arg_paren
@@ -70,6 +77,8 @@ module TestProf
70
77
 
71
78
  if args.first.first == :string_literal
72
79
  res.desc = parse_literal(args.shift)
80
+ elsif args.first.first == :var_ref || args.first.first == :const_path_ref
81
+ res.desc_const = parse_const(args.shift)
73
82
  end
74
83
 
75
84
  parse_arg(res, args.shift) until args.empty?
@@ -77,6 +86,7 @@ module TestProf
77
86
  res
78
87
  end
79
88
  # rubocop: enable Metrics/CyclomaticComplexity
89
+ # rubocop: enable Metrics/PerceivedComplexity
80
90
 
81
91
  private
82
92
 
@@ -90,7 +100,24 @@ module TestProf
90
100
 
91
101
  def parse_hash(res, hash_arg)
92
102
  hash_arg.each do |(_, label, val)|
93
- res.add_htag label[1][0..-2].to_sym, parse_literal(val)
103
+ res.add_htag label[1][0..-2].to_sym, parse_value(val)
104
+ end
105
+ end
106
+
107
+ # Expr of the form:
108
+ # bool - [:var_ref, [:@kw, "true", [1, 24]]]
109
+ # string - [:string_literal, [:string_content, [...]]]
110
+ # int - [:@int, "3", [1, 52]]]]
111
+ def parse_value(expr)
112
+ case expr.first
113
+ when :var_ref
114
+ expr[1][1] == "true"
115
+ when :@int
116
+ expr[1].to_i
117
+ when :@float
118
+ expr[1].to_f
119
+ else
120
+ parse_literal(expr)
94
121
  end
95
122
  end
96
123
 
@@ -102,6 +129,25 @@ module TestProf
102
129
  expr[0] == :assoc_new
103
130
  val
104
131
  end
132
+
133
+ # Expr of the form:
134
+ # [:var_ref, [:@const, "User", [1, 9]]]
135
+ #
136
+ # or
137
+ #
138
+ # [:const_path_ref, [:const_path_ref, [:var_ref,
139
+ # [:@const, "User", [1, 17]]],
140
+ # [:@const, "Guest", [1, 23]]],
141
+ # [:@const, "Collection", [1, 30]]
142
+ def parse_const(expr)
143
+ if expr.first == :var_ref
144
+ expr[1][1]
145
+ elsif expr.first == :@const
146
+ expr[1]
147
+ elsif expr.first == :const_path_ref
148
+ expr[1..-1].map(&method(:parse_const)).join("::")
149
+ end
150
+ end
105
151
  end
106
152
  end
107
153
  end
@@ -30,8 +30,10 @@ module TestProf
30
30
  end
31
31
 
32
32
  def stamp!
33
+ stamper = Stamper.new
34
+
33
35
  @examples.each do |file, lines|
34
- stamp_file(file, lines.uniq)
36
+ stamper.stamp_file(file, lines.uniq)
35
37
  end
36
38
 
37
39
  msgs = []
@@ -40,44 +42,15 @@ module TestProf
40
42
  <<-MSG.strip_heredoc
41
43
  RSpec Stamp results
42
44
 
43
- Total patches: #{@total}
45
+ Total patches: #{stamper.total}
44
46
  Total files: #{@examples.keys.size}
45
47
 
46
- Failed patches: #{@failed}
47
- Ignored files: #{@ignored}
48
+ Failed patches: #{stamper.failed}
49
+ Ignored files: #{stamper.ignored}
48
50
  MSG
49
51
 
50
52
  log :info, msgs.join
51
53
  end
52
-
53
- private
54
-
55
- def stamp_file(file, lines)
56
- @total += lines.size
57
- return if ignored?(file)
58
-
59
- log :info, "(dry-run) Patching #{file}" if dry_run?
60
-
61
- code = File.readlines(file)
62
-
63
- @failed += RSpecStamp.apply_tags(code, lines, RSpecStamp.config.tags)
64
-
65
- File.write(file, code.join) unless dry_run?
66
- end
67
-
68
- def ignored?(file)
69
- ignored = RSpecStamp.config.ignore_files.find do |pattern|
70
- file =~ pattern
71
- end
72
-
73
- return unless ignored
74
- log :warn, "Ignore stamping file: #{file}"
75
- @ignored += 1
76
- end
77
-
78
- def dry_run?
79
- RSpecStamp.config.dry_run?
80
- end
81
54
  end
82
55
  end
83
56
  end
@@ -10,7 +10,8 @@ module TestProf
10
10
 
11
11
  # RSpecStamp configuration
12
12
  class Configuration
13
- attr_accessor :ignore_files, :dry_run, :tags
13
+ attr_reader :tags
14
+ attr_accessor :ignore_files, :dry_run
14
15
 
15
16
  def initialize
16
17
  @ignore_files = [%r{spec/support}]
@@ -44,6 +45,49 @@ module TestProf
44
45
  end
45
46
  end
46
47
 
48
+ # Stamper collects statistics about applying tags
49
+ # to examples.
50
+ class Stamper
51
+ include TestProf::Logging
52
+
53
+ attr_reader :total, :failed, :ignored
54
+
55
+ def initialize
56
+ @total = 0
57
+ @failed = 0
58
+ @ignored = 0
59
+ end
60
+
61
+ def stamp_file(file, lines)
62
+ @total += lines.size
63
+ return if ignored?(file)
64
+
65
+ log :info, "(dry-run) Patching #{file}" if dry_run?
66
+
67
+ code = File.readlines(file)
68
+
69
+ @failed += RSpecStamp.apply_tags(code, lines, RSpecStamp.config.tags)
70
+
71
+ File.write(file, code.join) unless dry_run?
72
+ end
73
+
74
+ private
75
+
76
+ def ignored?(file)
77
+ ignored = RSpecStamp.config.ignore_files.find do |pattern|
78
+ file =~ pattern
79
+ end
80
+
81
+ return unless ignored
82
+ log :warn, "Ignore stamping file: #{file}"
83
+ @ignored += 1
84
+ end
85
+
86
+ def dry_run?
87
+ RSpecStamp.config.dry_run?
88
+ end
89
+ end
90
+
47
91
  class << self
48
92
  include TestProf::Logging
49
93
 
@@ -84,11 +128,11 @@ module TestProf
84
128
  parsed = Parser.parse(code)
85
129
  return false unless parsed
86
130
 
87
- parsed.desc ||= 'works'
131
+ desc = parsed.desc_const || quote(parsed.desc || 'works')
88
132
 
89
133
  tags.each do |t|
90
134
  if t.is_a?(Hash)
91
- t.keys.each do |k|
135
+ t.each_key do |k|
92
136
  parsed.remove_tag(k)
93
137
  parsed.add_htag(k, t[k])
94
138
  end
@@ -101,9 +145,9 @@ module TestProf
101
145
  need_parens = block == "{"
102
146
 
103
147
  tags_str = parsed.tags.map { |t| t.is_a?(Symbol) ? ":#{t}" : t }.join(", ") unless
104
- parsed.tags.nil?
148
+ parsed.tags.nil? || parsed.tags.empty?
105
149
 
106
- unless parsed.htags.nil?
150
+ unless parsed.htags.nil? || parsed.htags.empty?
107
151
  htags_str = parsed.htags.map do |(k, v)|
108
152
  vstr = v.is_a?(Symbol) ? ":#{v}" : quote(v)
109
153
 
@@ -112,7 +156,7 @@ module TestProf
112
156
  end
113
157
 
114
158
  replacement = "\\1#{parsed.fname}#{need_parens ? '(' : ' '}"\
115
- "#{[quote(parsed.desc), tags_str, htags_str].compact.join(', ')}"\
159
+ "#{[desc, tags_str, htags_str].compact.join(', ')}"\
116
160
  "#{need_parens ? ') ' : ' '}\\3"
117
161
 
118
162
  if config.dry_run?
@@ -126,6 +170,7 @@ module TestProf
126
170
  # rubocop: enable Metrics/PerceivedComplexity
127
171
 
128
172
  def quote(str)
173
+ return str unless str.is_a?(String)
129
174
  if str.include?("'")
130
175
  "\"#{str}\""
131
176
  else
@@ -50,7 +50,8 @@ module TestProf
50
50
  :include_threads, :eliminate_methods
51
51
 
52
52
  def initialize
53
- @printer = ENV.fetch('TEST_RUBY_PROF_PRINTER', :call_stack).to_sym
53
+ @printer = ENV['TEST_RUBY_PROF'].to_sym if PRINTERS.key?(ENV['TEST_RUBY_PROF'])
54
+ @printer ||= ENV.fetch('TEST_RUBY_PROF_PRINTER', :flat).to_sym
54
55
  @mode = ENV.fetch('TEST_RUBY_PROF_MODE', :wall).to_sym
55
56
  @min_percent = 1
56
57
  @include_threads = false
@@ -28,7 +28,7 @@ module TestProf
28
28
 
29
29
  def initialize
30
30
  @mode = ENV.fetch('TEST_STACK_PROF_MODE', :wall).to_sym
31
- @raw = ENV['TEST_STACK_PROF_RAW'] == '1'
31
+ @raw = ENV['TEST_STACK_PROF'] == 'raw' || ENV['TEST_STACK_PROF_RAW'] == 1
32
32
  end
33
33
  end
34
34
 
@@ -42,7 +42,11 @@ module TestProf
42
42
  end
43
43
 
44
44
  def each
45
- data.each
45
+ if block_given?
46
+ data.each(&Proc.new)
47
+ else
48
+ data.each
49
+ end
46
50
  end
47
51
 
48
52
  def size
@@ -50,7 +54,7 @@ module TestProf
50
54
  end
51
55
 
52
56
  def to_a
53
- data
57
+ data.dup
54
58
  end
55
59
 
56
60
  private
@@ -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".freeze
5
5
  end
data/lib/test_prof.rb CHANGED
@@ -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"
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
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-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0.49'
75
+ version: '0.50'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0.49'
82
+ version: '0.50'
83
83
  description: "\n Ruby applications tests profiling tools.\n\n Contains tools
84
84
  to anylyze factories usage, integrate with Ruby profilers,\n profile your examples
85
85
  using ActiveSupport notifications (if any) and\n statically analyze your code
@@ -108,6 +108,8 @@ files:
108
108
  - guides/factory_default.md
109
109
  - guides/factory_doctor.md
110
110
  - guides/factory_prof.md
111
+ - guides/let_it_be.md
112
+ - guides/rspec_dissect.md
111
113
  - guides/rspec_stamp.md
112
114
  - guides/rubocop.md
113
115
  - guides/ruby_prof.md
@@ -145,7 +147,10 @@ files:
145
147
  - lib/test_prof/recipes/rspec/any_fixture.rb
146
148
  - lib/test_prof/recipes/rspec/before_all.rb
147
149
  - lib/test_prof/recipes/rspec/factory_default.rb
150
+ - lib/test_prof/recipes/rspec/let_it_be.rb
148
151
  - lib/test_prof/recipes/rspec/sample.rb
152
+ - lib/test_prof/rspec_dissect.rb
153
+ - lib/test_prof/rspec_dissect/rspec.rb
149
154
  - lib/test_prof/rspec_stamp.rb
150
155
  - lib/test_prof/rspec_stamp/parser.rb
151
156
  - lib/test_prof/rspec_stamp/rspec.rb
@@ -179,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
184
  version: '0'
180
185
  requirements: []
181
186
  rubyforge_project:
182
- rubygems_version: 2.6.13
187
+ rubygems_version: 2.6.11
183
188
  signing_key:
184
189
  specification_version: 4
185
190
  summary: Ruby applications tests profiling tools