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