test-prof 0.2.1 → 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 +4 -4
- data/CHANGELOG.md +59 -1
- data/README.md +12 -2
- data/guides/event_prof.md +12 -0
- data/guides/factory_doctor.md +10 -0
- data/guides/let_it_be.md +96 -0
- data/guides/rspec_dissect.md +58 -0
- data/guides/ruby_prof.md +5 -5
- data/guides/stack_prof.md +3 -3
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +2 -2
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +5 -1
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +5 -1
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +5 -1
- data/lib/test_prof/event_prof/rspec.rb +44 -5
- data/lib/test_prof/event_prof.rb +38 -25
- data/lib/test_prof/ext/array_bsearch_index.rb +15 -0
- data/lib/test_prof/ext/float_duration.rb +0 -1
- data/lib/test_prof/ext/string_strip_heredoc.rb +15 -0
- data/lib/test_prof/ext/string_truncate.rb +19 -0
- data/lib/test_prof/factory_doctor/rspec.rb +38 -2
- data/lib/test_prof/factory_doctor.rb +12 -4
- data/lib/test_prof/factory_prof/printers/simple.rb +4 -1
- data/lib/test_prof/recipes/rspec/any_fixture.rb +1 -1
- data/lib/test_prof/recipes/rspec/before_all.rb +8 -0
- data/lib/test_prof/recipes/rspec/let_it_be.rb +80 -0
- data/lib/test_prof/rspec_dissect/rspec.rb +157 -0
- data/lib/test_prof/rspec_dissect.rb +112 -0
- data/lib/test_prof/rspec_stamp/parser.rb +58 -7
- data/lib/test_prof/rspec_stamp/rspec.rb +10 -34
- data/lib/test_prof/rspec_stamp.rb +55 -6
- data/lib/test_prof/ruby_prof.rb +19 -4
- data/lib/test_prof/stack_prof.rb +19 -5
- data/lib/test_prof/tag_prof/rspec.rb +5 -3
- data/lib/test_prof/utils/sized_ordered_set.rb +65 -0
- data/lib/test_prof/utils.rb +18 -0
- data/lib/test_prof/version.rb +1 -1
- data/lib/test_prof.rb +15 -2
- metadata +15 -75
|
@@ -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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TestProf
|
|
4
|
+
# Extend String with #truncate method
|
|
5
|
+
module StringTruncate
|
|
6
|
+
refine String do
|
|
7
|
+
# Truncate to the specified limit
|
|
8
|
+
# by replacing middle part with dots
|
|
9
|
+
def truncate(limit = 30)
|
|
10
|
+
return self unless size > limit
|
|
11
|
+
|
|
12
|
+
head = ((limit - 3) / 2)
|
|
13
|
+
tail = head + 3 - limit
|
|
14
|
+
|
|
15
|
+
"#{self[0..(head - 1)]}...#{self[tail..-1]}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -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
|
-
|
|
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
|
|
@@ -16,7 +16,7 @@ module TestProf
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def bad?
|
|
19
|
-
count
|
|
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
|
|
@@ -82,14 +90,14 @@ module TestProf
|
|
|
82
90
|
return yield if ignore? || !running? || (strategy != :create)
|
|
83
91
|
|
|
84
92
|
begin
|
|
85
|
-
ts =
|
|
93
|
+
ts = TestProf.now if @depth.zero?
|
|
86
94
|
@depth += 1
|
|
87
95
|
@count += 1
|
|
88
96
|
yield
|
|
89
97
|
ensure
|
|
90
98
|
@depth -= 1
|
|
91
99
|
|
|
92
|
-
@time += (
|
|
100
|
+
@time += (TestProf.now - ts) if @depth.zero?
|
|
93
101
|
end
|
|
94
102
|
end
|
|
95
103
|
|
|
@@ -111,7 +119,7 @@ module TestProf
|
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
def within_factory?
|
|
114
|
-
@depth
|
|
122
|
+
@depth > 0
|
|
115
123
|
end
|
|
116
124
|
|
|
117
125
|
def ignore?
|
|
@@ -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
|
-
|
|
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
|
|
15
|
+
pool.active_connection? && pool.connection.open_transactions > 0
|
|
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,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
|
|
@@ -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)
|
|
@@ -22,9 +20,16 @@ module TestProf
|
|
|
22
20
|
@htags ||= []
|
|
23
21
|
@htags << [k, v]
|
|
24
22
|
end
|
|
23
|
+
|
|
24
|
+
def remove_tag(tag)
|
|
25
|
+
@tags.delete(tag) if @tags
|
|
26
|
+
@htags.delete_if { |(k, _v)| k == tag } if @htags
|
|
27
|
+
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
class << self
|
|
31
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
32
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
28
33
|
def parse(code)
|
|
29
34
|
sexp = Ripper.sexp(code)
|
|
30
35
|
return unless sexp
|
|
@@ -53,11 +58,17 @@ module TestProf
|
|
|
53
58
|
res = Result.new
|
|
54
59
|
|
|
55
60
|
fcall = sexp[1][0][1]
|
|
56
|
-
fcall = fcall[1] if fcall.first == :fcall
|
|
57
|
-
res.fname = fcall[1]
|
|
58
|
-
|
|
59
61
|
args_block = sexp[1][0][2]
|
|
60
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
|
+
|
|
61
72
|
return res if args_block.nil?
|
|
62
73
|
|
|
63
74
|
args_block = args_block[1] if args_block.first == :arg_paren
|
|
@@ -66,12 +77,16 @@ module TestProf
|
|
|
66
77
|
|
|
67
78
|
if args.first.first == :string_literal
|
|
68
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)
|
|
69
82
|
end
|
|
70
83
|
|
|
71
84
|
parse_arg(res, args.shift) until args.empty?
|
|
72
85
|
|
|
73
86
|
res
|
|
74
87
|
end
|
|
88
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
89
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
75
90
|
|
|
76
91
|
private
|
|
77
92
|
|
|
@@ -85,7 +100,24 @@ module TestProf
|
|
|
85
100
|
|
|
86
101
|
def parse_hash(res, hash_arg)
|
|
87
102
|
hash_arg.each do |(_, label, val)|
|
|
88
|
-
res.add_htag label[1][0..-2].to_sym,
|
|
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)
|
|
89
121
|
end
|
|
90
122
|
end
|
|
91
123
|
|
|
@@ -97,6 +129,25 @@ module TestProf
|
|
|
97
129
|
expr[0] == :assoc_new
|
|
98
130
|
val
|
|
99
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
|
|
100
151
|
end
|
|
101
152
|
end
|
|
102
153
|
end
|