test-prof 0.3.0.pre2 → 0.3.0

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: 7e8016e347e65b9c7be1918f7d9b34f6cf0da920
4
- data.tar.gz: 93e9805bad937f7eb0e2e730274c61e58018c52a
3
+ metadata.gz: eb103d86ccc7a513de8b30ae8ddf536c9bbc6255
4
+ data.tar.gz: f0fe1c206faab5761b4419ccbf8745f4060d3e82
5
5
  SHA512:
6
- metadata.gz: c837ef686608e8110cd6224e4fa2a942e287c5dc492dd6d8eb21b58399feed435e5470b8b91abfd8cc6f767360b6dd6800156d91b70145dca141477875f150a6
7
- data.tar.gz: f800cc10fb99658fd3757259ca1e37afd2ccfab496b4f96bc455d09e781bdc2a2ed80128515ee8081918a933121af70643fa5f736ca4209b5824c3913dfbfeb8
6
+ metadata.gz: f6a4881cfc954438fa4184c2cd9be1784d10bfcd6d92931ea5132c8422aa08866b77e60ec91066ddd6afb620c524e99b4584818d158f396176f9649ef3a0e038
7
+ data.tar.gz: a699bd6e308b370aa4e53447025551a5b2f30249eb893356d30238eaed4dc135bb69335c780ea9d0b306ce569bca29117494ab91b6a70bbb7a22f714a65f827f
data/CHANGELOG.md CHANGED
@@ -2,7 +2,21 @@
2
2
 
3
3
  ## 0.3.0
4
4
 
5
- Features:
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.
6
20
 
7
21
  - [#14](https://github.com/palkan/test-prof/pull/14) RSpecDissect profiler. ([@palkan][])
8
22
 
@@ -13,6 +27,26 @@ and memoization helpers (i.e. `let`) in your tests.
13
27
 
14
28
  Just like `let`, but persist the result for the whole group (i.e. `let` + `before_all`).
15
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
+
40
+ ## 0.2.5
41
+
42
+ - [#16](https://github.com/palkan/test-prof/pull/16) Support Ruby >= 2.2.0 (was >= 2.3.0). ([@palkan][])
43
+
44
+ ## 0.2.4
45
+
46
+ - EventProf: Fix regression bug with examples profiling. ([@palkan][])
47
+
48
+ There was a bug when an event occurs before the example has started (e.g. in `before(:context)` hook).
49
+
16
50
  ## 0.2.3
17
51
 
18
52
  - Minor improvements. ([@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,10 +20,16 @@ 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
 
27
+ Supported Ruby versions:
28
+
29
+ - Ruby (MRI) >= 2.2.0
30
+
31
+ - JRuby >= 9.1.0.0
32
+
27
33
  <a href="https://evilmartians.com/">
28
34
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
29
35
 
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.
@@ -35,15 +35,24 @@ AvailableSlotsController (./spec/controllers/available_slots_controller_spec.rb:
35
35
 
36
36
  RSpecDissect can only be used with RSpec (which is clear from the name).
37
37
 
38
- To activate RSpecDissect use `RD` environment variable:
38
+ To activate RSpecDissect use `RD_PROF` environment variable:
39
39
 
40
40
  ```sh
41
- RD=1 rspec ...
41
+ RD_PROF=1 rspec ...
42
42
  ```
43
43
 
44
- You can also specify the number of top slow groups through `RD_TOP` variable:
44
+ You can also specify the number of top slow groups through `RD_PROF_TOP` variable:
45
45
 
46
46
  ```sh
47
- RD=1 RD_TOP=10 rspec ...
47
+ RD_PROF=1 RD_PROF_TOP=10 rspec ...
48
48
  ```
49
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.
@@ -39,7 +39,7 @@ module RuboCop
39
39
 
40
40
  def on_block(node)
41
41
  method, _args, body = *node
42
- return unless body&.begin_type?
42
+ return unless body && 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&.block_type? &&
112
+ node && node.block_type? &&
113
113
  (node.source.lines.size == 1) &&
114
114
  example_node?(node)
115
115
  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
@@ -105,7 +113,7 @@ module TestProf
105
113
  @time += time
106
114
  @count += 1
107
115
 
108
- return unless config.per_example?
116
+ return if @current_example.nil?
109
117
 
110
118
  @example_time += time
111
119
  @example_count += 1
@@ -124,8 +132,10 @@ module TestProf
124
132
  @current_group = nil
125
133
  end
126
134
 
127
- def example_started(_id)
128
- reset_example! if config.per_example?
135
+ def example_started(id)
136
+ return unless config.per_example?
137
+ reset_example!
138
+ @current_example = id
129
139
  end
130
140
 
131
141
  def example_finished(id)
@@ -134,6 +144,7 @@ module TestProf
134
144
 
135
145
  data = { id: id, time: @example_time, count: @example_count }
136
146
  @examples << data unless data[rank_by].zero?
147
+ @current_example = nil
137
148
  end
138
149
 
139
150
  def results
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
5
+ using TestProf::StringStripHeredoc
6
+
3
7
  module TestProf::EventProf::CustomEvents
4
8
  module FactoryCreate # :nodoc: all
5
9
  module RunnerPatch
@@ -42,7 +46,7 @@ end
42
46
  TestProf.activate('EVENT_PROF', 'factory.create') do
43
47
  if TestProf.require(
44
48
  'factory_girl',
45
- <<~MSG
49
+ <<-MSG.strip_heredoc
46
50
  Failed to load FactoryGirl.
47
51
 
48
52
  Make sure that "factory_girl" gem is in your Gemfile.
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
5
+ using TestProf::StringStripHeredoc
6
+
3
7
  module TestProf::EventProf::CustomEvents
4
8
  module SidekiqInline # :nodoc: all
5
9
  module ClientPatch
@@ -39,7 +43,7 @@ end
39
43
  TestProf.activate('EVENT_PROF', 'sidekiq.inline') do
40
44
  if TestProf.require(
41
45
  'sidekiq/testing',
42
- <<~MSG
46
+ <<-MSG.strip_heredoc
43
47
  Failed to load Sidekiq.
44
48
 
45
49
  Make sure that "sidekiq" gem is in your Gemfile.
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
5
+ using TestProf::StringStripHeredoc
6
+
3
7
  module TestProf::EventProf::CustomEvents
4
8
  module SidekiqJobs # :nodoc: all
5
9
  module ClientPatch
@@ -26,7 +30,7 @@ end
26
30
  TestProf.activate('EVENT_PROF', 'sidekiq.jobs') do
27
31
  if TestProf.require(
28
32
  'sidekiq/testing',
29
- <<~MSG
33
+ <<-MSG.strip_heredoc
30
34
  Failed to load Sidekiq.
31
35
 
32
36
  Make sure that "sidekiq" gem is in your Gemfile.
@@ -2,6 +2,7 @@
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"
5
6
 
6
7
  module TestProf
7
8
  module EventProf
@@ -9,6 +10,7 @@ module TestProf
9
10
  include Logging
10
11
  using FloatDuration
11
12
  using StringTruncate
13
+ using StringStripHeredoc
12
14
 
13
15
  NOTIFICATIONS = %i[
14
16
  example_group_started
@@ -45,7 +47,7 @@ module TestProf
45
47
  msgs = []
46
48
 
47
49
  msgs <<
48
- <<~MSG
50
+ <<-MSG.strip_heredoc
49
51
  EventProf results for #{@profiler.event}
50
52
 
51
53
  Total time: #{@profiler.total_time.duration}
@@ -60,7 +62,7 @@ module TestProf
60
62
  location = group[:id].metadata[:location]
61
63
 
62
64
  msgs <<
63
- <<~GROUP
65
+ <<-GROUP.strip_heredoc
64
66
  #{description.truncate} (#{location}) – #{group[:time].duration} (#{group[:count]} / #{group[:examples]})
65
67
  GROUP
66
68
  end
@@ -72,13 +74,48 @@ module TestProf
72
74
  description = example[:id].description
73
75
  location = example[:id].metadata[:location]
74
76
  msgs <<
75
- <<~GROUP
77
+ <<-GROUP.strip_heredoc
76
78
  #{description.truncate} (#{location}) – #{example[:time].duration} (#{example[:count]})
77
79
  GROUP
78
80
  end
79
81
  end
80
82
 
81
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
82
119
  end
83
120
  end
84
121
  end
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
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
@@ -16,7 +16,7 @@ module TestProf
16
16
  end
17
17
 
18
18
  def bad?
19
- count.positive? && queries_count.zero?
19
+ count > 0 && queries_count.zero?
20
20
  end
21
21
  end
22
22
 
@@ -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
@@ -111,7 +119,7 @@ module TestProf
111
119
  end
112
120
 
113
121
  def within_factory?
114
- @depth.positive?
122
+ @depth > 0
115
123
  end
116
124
 
117
125
  def ignore?
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/ext/float_duration"
4
+ require "test_prof/ext/string_strip_heredoc"
4
5
 
5
6
  module TestProf
6
7
  module FactoryDoctor
7
8
  class RSpecListener # :nodoc:
8
9
  include Logging
9
10
  using FloatDuration
11
+ using StringStripHeredoc
10
12
 
11
- SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'
13
+ SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'.freeze
12
14
 
13
15
  NOTIFICATIONS = %i[
14
16
  example_started
@@ -49,7 +51,7 @@ module TestProf
49
51
  msgs = []
50
52
 
51
53
  msgs <<
52
- <<~MSG
54
+ <<-MSG.strip_heredoc
53
55
  FactoryDoctor report
54
56
 
55
57
  Total (potentially) bad examples: #{@count}
@@ -68,6 +70,40 @@ module TestProf
68
70
  end
69
71
 
70
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
71
107
  end
72
108
 
73
109
  private
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
3
5
  module TestProf::FactoryProf
4
6
  module Printers
5
7
  module Simple # :nodoc: all
6
8
  class << self
7
9
  include TestProf::Logging
10
+ using TestProf::StringStripHeredoc
8
11
 
9
12
  def dump(result)
10
13
  msgs = []
11
14
 
12
15
  msgs <<
13
- <<~MSG
16
+ <<-MSG.strip_heredoc
14
17
  Factories usage
15
18
 
16
19
  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.positive?
15
+ pool.active_connection? && pool.connection.open_transactions > 0
16
16
  end
17
17
  end
18
18
 
@@ -22,7 +22,7 @@ module TestProf
22
22
  # Use uniq prefix for instance variables to avoid collisions
23
23
  # We want to use the power of Ruby's unicode support)
24
24
  # And we love cats!)
25
- PREFIX = "@😸"
25
+ PREFIX = RUBY_ENGINE == 'jruby' ? "@__jruby_is_not_cat_friendly__".freeze : "@😸".freeze
26
26
 
27
27
  def let_it_be(identifier, **options, &block)
28
28
  initializer = proc do
@@ -41,7 +41,13 @@ module TestProf
41
41
  def define_let_it_be_methods(identifier, reload: false, refind: false)
42
42
  let_accessor = -> { instance_variable_get(:"#{PREFIX}#{identifier}") }
43
43
 
44
- let_accessor = -> { instance_variable_get(:"#{PREFIX}#{identifier}")&.reload } if reload
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
45
51
 
46
52
  if refind
47
53
  let_accessor = lambda do
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/rspec_stamp"
3
4
  require "test_prof/logging"
4
5
 
5
6
  module TestProf
@@ -35,7 +36,14 @@ module TestProf
35
36
  attr_accessor :top_count
36
37
 
37
38
  def initialize
38
- @top_count = (ENV['RD_TOP'] || 5).to_i
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?
39
47
  end
40
48
  end
41
49
 
@@ -99,6 +107,6 @@ end
99
107
 
100
108
  require "test_prof/rspec_dissect/rspec" if defined?(RSpec::Core)
101
109
 
102
- TestProf.activate('RD') do
110
+ TestProf.activate('RD_PROF') do
103
111
  TestProf::RSpecDissect.init
104
112
  end
@@ -3,6 +3,7 @@
3
3
  require "test_prof/ext/float_duration"
4
4
  require "test_prof/ext/string_truncate"
5
5
  require "test_prof/utils/sized_ordered_set"
6
+ require "test_prof/ext/string_strip_heredoc"
6
7
 
7
8
  module TestProf
8
9
  module RSpecDissect
@@ -10,6 +11,7 @@ module TestProf
10
11
  include Logging
11
12
  using FloatDuration
12
13
  using StringTruncate
14
+ using StringStripHeredoc
13
15
 
14
16
  NOTIFICATIONS = %i[
15
17
  example_finished
@@ -58,7 +60,7 @@ module TestProf
58
60
  msgs = []
59
61
 
60
62
  msgs <<
61
- <<~MSG
63
+ <<-MSG.strip_heredoc
62
64
  RSpecDissect report
63
65
 
64
66
  Total time: #{@total_examples_time.duration}
@@ -68,32 +70,66 @@ module TestProf
68
70
  MSG
69
71
 
70
72
  msgs <<
71
- <<~MSG
73
+ <<-MSG.strip_heredoc
72
74
  Top #{top_count} slowest suites (by `before(:each)` time):
73
75
 
74
76
  MSG
75
77
 
76
78
  @before_results.each do |group|
77
79
  msgs <<
78
- <<~GROUP
80
+ <<-GROUP.strip_heredoc
79
81
  #{group[:desc].truncate} (#{group[:loc]}) – #{group[:before].duration} of #{group[:total].duration} (#{group[:count]})
80
82
  GROUP
81
83
  end
82
84
 
83
85
  msgs <<
84
- <<~MSG
86
+ <<-MSG.strip_heredoc
87
+
85
88
  Top #{top_count} slowest suites (by `let` time):
86
89
 
87
90
  MSG
88
91
 
89
92
  @memo_results.each do |group|
90
93
  msgs <<
91
- <<~GROUP
94
+ <<-GROUP.strip_heredoc
92
95
  #{group[:desc].truncate} (#{group[:loc]}) – #{group[:memo].duration} of #{group[:total].duration} (#{group[:count]})
93
96
  GROUP
94
97
  end
95
98
 
96
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
97
133
  end
98
134
 
99
135
  private
@@ -106,7 +142,7 @@ module TestProf
106
142
  end
107
143
 
108
144
  # Register RSpecDissect listener
109
- TestProf.activate('RD') do
145
+ TestProf.activate('RD_PROF') do
110
146
  RSpec.configure do |config|
111
147
  listener = TestProf::RSpecDissect::Listener.new
112
148
 
@@ -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
@@ -2,15 +2,13 @@
2
2
 
3
3
  require "ripper"
4
4
 
5
- # rubocop: disable Metrics/CyclomaticComplexity
6
-
7
5
  module TestProf
8
6
  module RSpecStamp
9
7
  # Parse examples headers
10
8
  module Parser
11
9
  # Contains the result of parsing
12
10
  class Result
13
- attr_accessor :fname, :desc
11
+ attr_accessor :fname, :desc, :desc_const
14
12
  attr_reader :tags, :htags
15
13
 
16
14
  def add_tag(v)
@@ -24,12 +22,14 @@ module TestProf
24
22
  end
25
23
 
26
24
  def remove_tag(tag)
27
- @tags&.delete(tag)
28
- @htags&.delete_if { |(k, _v)| k == tag }
25
+ @tags.delete(tag) if @tags
26
+ @htags.delete_if { |(k, _v)| k == tag } if @htags
29
27
  end
30
28
  end
31
29
 
32
30
  class << self
31
+ # rubocop: disable Metrics/CyclomaticComplexity
32
+ # rubocop: disable Metrics/PerceivedComplexity
33
33
  def parse(code)
34
34
  sexp = Ripper.sexp(code)
35
35
  return unless sexp
@@ -58,11 +58,17 @@ module TestProf
58
58
  res = Result.new
59
59
 
60
60
  fcall = sexp[1][0][1]
61
- fcall = fcall[1] if fcall.first == :fcall
62
- res.fname = fcall[1]
63
-
64
61
  args_block = sexp[1][0][2]
65
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
+
66
72
  return res if args_block.nil?
67
73
 
68
74
  args_block = args_block[1] if args_block.first == :arg_paren
@@ -71,12 +77,16 @@ module TestProf
71
77
 
72
78
  if args.first.first == :string_literal
73
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)
74
82
  end
75
83
 
76
84
  parse_arg(res, args.shift) until args.empty?
77
85
 
78
86
  res
79
87
  end
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
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
3
5
  module TestProf
4
6
  module RSpecStamp
5
7
  class RSpecListener # :nodoc:
6
8
  include Logging
9
+ using StringStripHeredoc
7
10
 
8
11
  NOTIFICATIONS = %i[
9
12
  example_failed
@@ -27,54 +30,27 @@ module TestProf
27
30
  end
28
31
 
29
32
  def stamp!
33
+ stamper = Stamper.new
34
+
30
35
  @examples.each do |file, lines|
31
- stamp_file(file, lines.uniq)
36
+ stamper.stamp_file(file, lines.uniq)
32
37
  end
33
38
 
34
39
  msgs = []
35
40
 
36
41
  msgs <<
37
- <<~MSG
42
+ <<-MSG.strip_heredoc
38
43
  RSpec Stamp results
39
44
 
40
- Total patches: #{@total}
45
+ Total patches: #{stamper.total}
41
46
  Total files: #{@examples.keys.size}
42
47
 
43
- Failed patches: #{@failed}
44
- Ignored files: #{@ignored}
48
+ Failed patches: #{stamper.failed}
49
+ Ignored files: #{stamper.ignored}
45
50
  MSG
46
51
 
47
52
  log :info, msgs.join
48
53
  end
49
-
50
- private
51
-
52
- def stamp_file(file, lines)
53
- @total += lines.size
54
- return if ignored?(file)
55
-
56
- log :info, "(dry-run) Patching #{file}" if dry_run?
57
-
58
- code = File.readlines(file)
59
-
60
- @failed += RSpecStamp.apply_tags(code, lines, RSpecStamp.config.tags)
61
-
62
- File.write(file, code.join) unless dry_run?
63
- end
64
-
65
- def ignored?(file)
66
- ignored = RSpecStamp.config.ignore_files.find do |pattern|
67
- file =~ pattern
68
- end
69
-
70
- return unless ignored
71
- log :warn, "Ignore stamping file: #{file}"
72
- @ignored += 1
73
- end
74
-
75
- def dry_run?
76
- RSpecStamp.config.dry_run?
77
- end
78
54
  end
79
55
  end
80
56
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
3
5
  module TestProf
4
6
  # RubyProf wrapper.
5
7
  #
@@ -48,7 +50,8 @@ module TestProf
48
50
  :include_threads, :eliminate_methods
49
51
 
50
52
  def initialize
51
- @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
52
55
  @mode = ENV.fetch('TEST_RUBY_PROF_MODE', :wall).to_sym
53
56
  @min_percent = 1
54
57
  @include_threads = false
@@ -119,6 +122,7 @@ module TestProf
119
122
 
120
123
  class << self
121
124
  include Logging
125
+ using StringStripHeredoc
122
126
 
123
127
  def config
124
128
  @config ||= Configuration.new
@@ -172,7 +176,7 @@ module TestProf
172
176
  ENV["RUBY_PROF_MEASURE_MODE"] = config.mode.to_s
173
177
  @initialized = TestProf.require(
174
178
  'ruby-prof',
175
- <<~MSG
179
+ <<-MSG.strip_heredoc
176
180
  Please, install 'ruby-prof' first:
177
181
  # Gemfile
178
182
  gem 'ruby-prof', '>= 0.16.0', require: false
@@ -184,7 +188,7 @@ module TestProf
184
188
  if Utils.verify_gem_version('ruby-prof', at_least: '0.16.0')
185
189
  true
186
190
  else
187
- log :error, <<~MGS
191
+ log :error, <<-MGS.strip_heredoc
188
192
  Please, upgrade 'ruby-prof' to version >= 0.16.0.
189
193
  MGS
190
194
  false
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
3
5
  module TestProf
4
6
  # StackProf wrapper.
5
7
  #
@@ -26,12 +28,13 @@ module TestProf
26
28
 
27
29
  def initialize
28
30
  @mode = ENV.fetch('TEST_STACK_PROF_MODE', :wall).to_sym
29
- @raw = ENV['TEST_STACK_PROF_RAW'] == '1'
31
+ @raw = ENV['TEST_STACK_PROF'] == 'raw' || ENV['TEST_STACK_PROF_RAW'] == 1
30
32
  end
31
33
  end
32
34
 
33
35
  class << self
34
36
  include Logging
37
+ using StringStripHeredoc
35
38
 
36
39
  def config
37
40
  @config ||= Configuration.new
@@ -88,7 +91,7 @@ module TestProf
88
91
 
89
92
  html_path = path.gsub(/\.dump$/, '.html')
90
93
 
91
- log :info, <<~MSG
94
+ log :info, <<-MSG.strip_heredoc
92
95
  Run the following command to generate a flame graph report:
93
96
 
94
97
  stackprof --flamegraph #{path} > #{html_path} && stackprof --flamegraph-viewer=#{html_path}
@@ -111,7 +114,7 @@ module TestProf
111
114
  return @initialized if instance_variable_defined?(:@initialized)
112
115
  @initialized = TestProf.require(
113
116
  'stackprof',
114
- <<~MSG
117
+ <<-MSG.strip_heredoc
115
118
  Please, install 'stackprof' first:
116
119
  # Gemfile
117
120
  gem 'stackprof', '>= 0.2.9', require: false
@@ -123,9 +126,9 @@ module TestProf
123
126
  if Utils.verify_gem_version('stackprof', at_least: '0.2.9')
124
127
  true
125
128
  else
126
- log :error, <<~MGS
129
+ log :error, <<-MSG.strip_heredoc
127
130
  Please, upgrade 'stackprof' to version >= 0.2.9.
128
- MGS
131
+ MSG
129
132
  false
130
133
  end
131
134
  end
@@ -1,12 +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"
4
5
 
5
6
  module TestProf
6
7
  module TagProf
7
8
  class RSpecListener # :nodoc:
8
9
  include Logging
9
10
  using FloatDuration
11
+ using StringStripHeredoc
10
12
 
11
13
  NOTIFICATIONS = %i[
12
14
  example_started
@@ -37,7 +39,7 @@ module TestProf
37
39
  msgs = []
38
40
 
39
41
  msgs <<
40
- <<~MSG
42
+ <<-MSG.strip_heredoc
41
43
  TagProf report for #{@tag}
42
44
  MSG
43
45
 
@@ -4,6 +4,11 @@ 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
+
7
12
  include Enumerable
8
13
 
9
14
  def initialize(max_size, sort_by: nil)
@@ -49,7 +54,7 @@ module TestProf
49
54
  end
50
55
 
51
56
  def to_a
52
- data
57
+ data.dup
53
58
  end
54
59
 
55
60
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "0.3.0.pre2"
4
+ VERSION = "0.3.0".freeze
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.3.0.pre2
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-28 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,28 +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'
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'
82
+ version: '0.50'
97
83
  description: "\n Ruby applications tests profiling tools.\n\n Contains tools
98
84
  to anylyze factories usage, integrate with Ruby profilers,\n profile your examples
99
85
  using ActiveSupport notifications (if any) and\n statically analyze your code
@@ -142,7 +128,9 @@ files:
142
128
  - lib/test_prof/event_prof/instrumentations/active_support.rb
143
129
  - lib/test_prof/event_prof/minitest.rb
144
130
  - lib/test_prof/event_prof/rspec.rb
131
+ - lib/test_prof/ext/array_bsearch_index.rb
145
132
  - lib/test_prof/ext/float_duration.rb
133
+ - lib/test_prof/ext/string_strip_heredoc.rb
146
134
  - lib/test_prof/ext/string_truncate.rb
147
135
  - lib/test_prof/factory_default.rb
148
136
  - lib/test_prof/factory_default/factory_girl_patch.rb
@@ -188,12 +176,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
188
176
  requirements:
189
177
  - - ">="
190
178
  - !ruby/object:Gem::Version
191
- version: 2.3.0
179
+ version: 2.2.0
192
180
  required_rubygems_version: !ruby/object:Gem::Requirement
193
181
  requirements:
194
- - - ">"
182
+ - - ">="
195
183
  - !ruby/object:Gem::Version
196
- version: 1.3.1
184
+ version: '0'
197
185
  requirements: []
198
186
  rubyforge_project:
199
187
  rubygems_version: 2.6.11