test-prof 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +6 -2
- data/guides/event_prof.md +10 -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 +4 -4
- data/guides/stack_prof.md +1 -1
- data/lib/test_prof/event_prof/rspec.rb +35 -0
- data/lib/test_prof/event_prof.rb +9 -1
- data/lib/test_prof/factory_doctor/rspec.rb +34 -0
- data/lib/test_prof/factory_doctor.rb +8 -0
- 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 +51 -5
- data/lib/test_prof/rspec_stamp/rspec.rb +6 -33
- data/lib/test_prof/rspec_stamp.rb +51 -6
- data/lib/test_prof/ruby_prof.rb +2 -1
- data/lib/test_prof/stack_prof.rb +1 -1
- data/lib/test_prof/utils/sized_ordered_set.rb +6 -2
- data/lib/test_prof/version.rb +1 -1
- data/lib/test_prof.rb +1 -0
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb103d86ccc7a513de8b30ae8ddf536c9bbc6255
|
|
4
|
+
data.tar.gz: f0fe1c206faab5761b4419ccbf8745f4060d3e82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6a4881cfc954438fa4184c2cd9be1784d10bfcd6d92931ea5132c8422aa08866b77e60ec91066ddd6afb620c524e99b4584818d158f396176f9649ef3a0e038
|
|
7
|
+
data.tar.gz: a699bd6e308b370aa4e53447025551a5b2f30249eb893356d30238eaed4dc135bb69335c780ea9d0b306ce569bca29117494ab91b6a70bbb7a22f714a65f827f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Features:
|
|
6
|
+
|
|
7
|
+
- Combine RSpecStamp with FactoryDoctor. ([@palkan][])
|
|
8
|
+
|
|
9
|
+
Automatically mark _bad_ examples with custom tags.
|
|
10
|
+
|
|
11
|
+
- [#17](https://github.com/palkan/test-prof/pull/17) Combine RSpecStamp with EventProf and RSpecDissect. ([@palkan][])
|
|
12
|
+
|
|
13
|
+
It is possible now to automatically mark _slow_ examples and groups with custom tags. For example:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
After running the command above the top 5 slowest example groups would be marked with `slow: :sql` tag.
|
|
20
|
+
|
|
21
|
+
- [#14](https://github.com/palkan/test-prof/pull/14) RSpecDissect profiler. ([@palkan][])
|
|
22
|
+
|
|
23
|
+
RSpecDissect tracks how much time do you spend in `before` hooks
|
|
24
|
+
and memoization helpers (i.e. `let`) in your tests.
|
|
25
|
+
|
|
26
|
+
- [#13](https://github.com/palkan/test-prof/pull/13) RSpec `let_it_be` method. ([@palkan][])
|
|
27
|
+
|
|
28
|
+
Just like `let`, but persist the result for the whole group (i.e. `let` + `before_all`).
|
|
29
|
+
|
|
30
|
+
### Improvements:
|
|
31
|
+
|
|
32
|
+
- Add ability to specify RubyProf report through `TEST_RUBY_PROF` env variable. ([@palkan][])
|
|
33
|
+
|
|
34
|
+
- Add ability to specify StackProf raw mode through `TEST_STACK_PROF` env variable. ([@palkan][])
|
|
35
|
+
|
|
36
|
+
### Changes
|
|
37
|
+
|
|
38
|
+
- Use RubyProf `FlatPrinter` by default (was `CallStackPrinter`). ([@palkan][])
|
|
39
|
+
|
|
3
40
|
## 0.2.5
|
|
4
41
|
|
|
5
42
|
- [#16](https://github.com/palkan/test-prof/pull/16) Support Ruby >= 2.2.0 (was >= 2.3.0). ([@palkan][])
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Simply speaking, slow tests waste your time making you less productive.
|
|
|
10
10
|
|
|
11
11
|
TestProf toolbox aims to help you identify bottlenecks in your test suite. It contains:
|
|
12
12
|
|
|
13
|
-
- Plug'n'Play integrations for general Ruby profilers ([`ruby-prof`](https://github.com/ruby-prof), [`stackprof`](https://github.com/tmm1/stackprof))
|
|
13
|
+
- Plug'n'Play integrations for general Ruby profilers ([`ruby-prof`](https://github.com/ruby-prof/ruby-prof), [`stackprof`](https://github.com/tmm1/stackprof))
|
|
14
14
|
|
|
15
15
|
- Factories usage analyzers and profilers
|
|
16
16
|
|
|
@@ -20,7 +20,7 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
|
|
|
20
20
|
|
|
21
21
|
- etc.
|
|
22
22
|
|
|
23
|
-
Of course, we have some [solutions](#tips-and-tricks) for common performance issues too, bundled into the gem.
|
|
23
|
+
Of course, we have some [solutions](#tips-and-tricks-or-recipes) for common performance issues too, bundled into the gem.
|
|
24
24
|
|
|
25
25
|
See [Table of Contents](#table-of-contents) for more.
|
|
26
26
|
|
|
@@ -69,6 +69,8 @@ Checkout our guides for each specific tool:
|
|
|
69
69
|
|
|
70
70
|
- [Factory Profiler](https://github.com/palkan/test-prof/tree/master/guides/factory_prof.md)
|
|
71
71
|
|
|
72
|
+
- [RSpecDissect Profiler](https://github.com/palkan/test-prof/tree/master/guides/rspec_dissect.md)
|
|
73
|
+
|
|
72
74
|
- [Rubocop Cops](https://github.com/palkan/test-prof/tree/master/guides/rubocop.md)
|
|
73
75
|
|
|
74
76
|
## Tips and Tricks (or _Recipes_)
|
|
@@ -77,6 +79,8 @@ We also want to share some small code tricks which can help you to improve your
|
|
|
77
79
|
|
|
78
80
|
- [`before_all` Hook](https://github.com/palkan/test-prof/tree/master/guides/before_all.md)
|
|
79
81
|
|
|
82
|
+
- [`let_it_be` Helper](https://github.com/palkan/test-prof/tree/master/guides/let_it_be.md)
|
|
83
|
+
|
|
80
84
|
- [AnyFixture](https://github.com/palkan/test-prof/tree/master/guides/any_fixture.md)
|
|
81
85
|
|
|
82
86
|
- [FactoryDefault](https://github.com/palkan/test-prof/tree/master/guides/factory_default.md)
|
data/guides/event_prof.md
CHANGED
|
@@ -66,6 +66,16 @@ EVENT_PROF_RANK=count EVENT_PROF='instantiation.active_record' be rspec
|
|
|
66
66
|
|
|
67
67
|
See [event_prof.rb](https://github.com/palkan/test-prof/tree/master/lib/test_prof/event_prof.rb) for all available configuration options and their usage.
|
|
68
68
|
|
|
69
|
+
## Using with RSpecStamp
|
|
70
|
+
|
|
71
|
+
EventProf can be used with [RSpec Stamp](https://github.com/palkan/test-prof/tree/master/guides/rspec_stamp.md) to automatically mark _slow_ examples with custom tags. For example:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After running the command above the slowest example groups (and examples if configured) would be marked with the `slow: :sql` tag.
|
|
78
|
+
|
|
69
79
|
## Custom Instrumentation
|
|
70
80
|
|
|
71
81
|
To use EventProf with your instrumentation engine just complete the two following steps:
|
data/guides/factory_doctor.md
CHANGED
|
@@ -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.
|
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,58 @@
|
|
|
1
|
+
# RSpecDissect
|
|
2
|
+
|
|
3
|
+
Do you know how much time you spend in `before` hooks? Or memoization helpers such as `let`? Usually, the most of the whole test suite time.
|
|
4
|
+
|
|
5
|
+
_RSpecDissect_ provides this kind of information and also shows you the worst example groups. The main purpose of RSpecDissect is to identify these slow groups and refactor them using [`before_all`](https://github.com/palkan/test-prof/tree/master/guides/before_all.md) or [`let_it_be`](https://github.com/palkan/test-prof/tree/master/guides/let_it_be.md) recipes.
|
|
6
|
+
|
|
7
|
+
Example output:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
[TEST PROF INFO] RSpecDissect enabled
|
|
11
|
+
|
|
12
|
+
Total time: 25:14.870
|
|
13
|
+
Total `before(:each)` time: 14:36.482
|
|
14
|
+
Total `let` time: 19:20.259
|
|
15
|
+
|
|
16
|
+
Top 5 slowest suites (by `before(:each)` time):
|
|
17
|
+
|
|
18
|
+
Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:29.895 of 00:33.706 (327)
|
|
19
|
+
FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:22.117 of 00:43.649 (133)
|
|
20
|
+
ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:21.220 of 00:41.407 (222)
|
|
21
|
+
BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:15.729 of 00:27.893 (50)
|
|
22
|
+
Analytics::Wor...rsion::Summary (./spec/services/analytics/workflow_conversion/summary_spec.rb:3) – 00:15.383 of 00:15.914 (12)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Top 5 slowest suites (by `let` time):
|
|
26
|
+
|
|
27
|
+
FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:38.532 of 00:43.649 (133)
|
|
28
|
+
ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:33.252 of 00:41.407 (222)
|
|
29
|
+
Webhooks::DispatchTransition (./spec/services/webhooks/dispatch_transition_spec.rb:3) – 00:30.320 of 00:33.706 (327)
|
|
30
|
+
BookedSlotsController (./spec/controllers/booked_slots_controller_spec.rb:3) – 00:25.710 of 00:27.893 (50)
|
|
31
|
+
AvailableSlotsController (./spec/controllers/available_slots_controller_spec.rb:3) – 00:18.481 of 00:23.366 (85)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Instructions
|
|
35
|
+
|
|
36
|
+
RSpecDissect can only be used with RSpec (which is clear from the name).
|
|
37
|
+
|
|
38
|
+
To activate RSpecDissect use `RD_PROF` environment variable:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
RD_PROF=1 rspec ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can also specify the number of top slow groups through `RD_PROF_TOP` variable:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
RD_PROF=1 RD_PROF_TOP=10 rspec ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Using with RSpecStamp
|
|
51
|
+
|
|
52
|
+
RSpecDissect can be used with [RSpec Stamp](https://github.com/palkan/test-prof/tree/master/guides/rspec_stamp.md) to automatically mark _slow_ examples with custom tags. For example:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
RD_PROF=1 RD_PROF_STAMP="slow" rspec ...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After running the command above the slowest example groups would be marked with the `:slow` tag.
|
data/guides/ruby_prof.md
CHANGED
|
@@ -42,21 +42,21 @@ end
|
|
|
42
42
|
|
|
43
43
|
The most useful configuration option is `printer` – it allows you to specify a RubyProf [printer](https://github.com/ruby-prof/ruby-prof#printers).
|
|
44
44
|
|
|
45
|
-
You can specify a printer through environment variable `
|
|
45
|
+
You can specify a printer through environment variable `TEST_RUBY_PROF`:
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
|
|
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 = :
|
|
55
|
+
config.printer = :call_stack
|
|
56
56
|
end
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
By default, we use `
|
|
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 `
|
|
45
|
+
If you want to generate flame graphs you should collect _raw_ data. Turn _raw_ collection on by passing `TEST_STACK_PROF=raw`.
|
|
46
46
|
|
|
47
47
|
See [stack_prof.rb](https://github.com/palkan/test-prof/tree/master/lib/test_prof/stack_prof.rb) for all available configuration options and their usage.
|
|
@@ -81,6 +81,41 @@ module TestProf
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
log :info, msgs.join
|
|
84
|
+
|
|
85
|
+
stamp! if EventProf.config.stamp?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stamp!
|
|
89
|
+
result = @profiler.results
|
|
90
|
+
|
|
91
|
+
stamper = RSpecStamp::Stamper.new
|
|
92
|
+
|
|
93
|
+
examples = Hash.new { |h, k| h[k] = [] }
|
|
94
|
+
|
|
95
|
+
(result[:groups].to_a + result.fetch(:examples, []).to_a)
|
|
96
|
+
.map { |obj| obj[:id].metadata[:location] }.each do |location|
|
|
97
|
+
file, line = location.split(":")
|
|
98
|
+
examples[file] << line.to_i
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
examples.each do |file, lines|
|
|
102
|
+
stamper.stamp_file(file, lines.uniq)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
msgs = []
|
|
106
|
+
|
|
107
|
+
msgs <<
|
|
108
|
+
<<-MSG.strip_heredoc
|
|
109
|
+
RSpec Stamp results
|
|
110
|
+
|
|
111
|
+
Total patches: #{stamper.total}
|
|
112
|
+
Total files: #{examples.keys.size}
|
|
113
|
+
|
|
114
|
+
Failed patches: #{stamper.failed}
|
|
115
|
+
Ignored files: #{stamper.ignored}
|
|
116
|
+
MSG
|
|
117
|
+
|
|
118
|
+
log :info, msgs.join
|
|
84
119
|
end
|
|
85
120
|
end
|
|
86
121
|
end
|
data/lib/test_prof/event_prof.rb
CHANGED
|
@@ -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, :
|
|
87
|
+
attr_reader :event, :total_count, :total_time
|
|
80
88
|
|
|
81
89
|
def initialize(event:, instrumenter:)
|
|
82
90
|
@event = event
|
|
@@ -70,6 +70,40 @@ module TestProf
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
log :info, msgs.join
|
|
73
|
+
|
|
74
|
+
stamp! if FactoryDoctor.stamp?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def stamp!
|
|
78
|
+
stamper = RSpecStamp::Stamper.new
|
|
79
|
+
|
|
80
|
+
examples = Hash.new { |h, k| h[k] = [] }
|
|
81
|
+
|
|
82
|
+
@example_groups.each_value do |bad_examples|
|
|
83
|
+
bad_examples.each do |example|
|
|
84
|
+
file, line = example.metadata[:location].split(":")
|
|
85
|
+
examples[file] << line.to_i
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
examples.each do |file, lines|
|
|
90
|
+
stamper.stamp_file(file, lines.uniq)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
msgs = []
|
|
94
|
+
|
|
95
|
+
msgs <<
|
|
96
|
+
<<-MSG.strip_heredoc
|
|
97
|
+
RSpec Stamp results
|
|
98
|
+
|
|
99
|
+
Total patches: #{stamper.total}
|
|
100
|
+
Total files: #{examples.keys.size}
|
|
101
|
+
|
|
102
|
+
Failed patches: #{stamper.failed}
|
|
103
|
+
Ignored files: #{stamper.ignored}
|
|
104
|
+
MSG
|
|
105
|
+
|
|
106
|
+
log :info, msgs.join
|
|
73
107
|
end
|
|
74
108
|
|
|
75
109
|
private
|
|
@@ -6,6 +6,10 @@ module TestProf
|
|
|
6
6
|
def before_all(&block)
|
|
7
7
|
raise ArgumentError, "Block is required!" unless block_given?
|
|
8
8
|
|
|
9
|
+
return if within_before_all?
|
|
10
|
+
|
|
11
|
+
@__before_all_activated__ = true
|
|
12
|
+
|
|
9
13
|
before(:all) do
|
|
10
14
|
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
|
11
15
|
instance_eval(&block)
|
|
@@ -15,6 +19,10 @@ module TestProf
|
|
|
15
19
|
ActiveRecord::Base.connection.rollback_transaction
|
|
16
20
|
end
|
|
17
21
|
end
|
|
22
|
+
|
|
23
|
+
def within_before_all?
|
|
24
|
+
instance_variable_defined?(:@__before_all_activated__)
|
|
25
|
+
end
|
|
18
26
|
end
|
|
19
27
|
end
|
|
20
28
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "./before_all"
|
|
4
|
+
|
|
5
|
+
module TestProf
|
|
6
|
+
# Just like `let`, but persist the result for the whole group.
|
|
7
|
+
# NOTE: Experimental and magical, for more control use `before_all`.
|
|
8
|
+
module LetItBe
|
|
9
|
+
class << self
|
|
10
|
+
def module_for(group)
|
|
11
|
+
modules.fetch(group) do
|
|
12
|
+
Module.new.tap { |mod| group.prepend(mod) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def modules
|
|
19
|
+
@modules ||= {}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
# Use uniq prefix for instance variables to avoid collisions
|
|
23
|
+
# We want to use the power of Ruby's unicode support)
|
|
24
|
+
# And we love cats!)
|
|
25
|
+
PREFIX = RUBY_ENGINE == 'jruby' ? "@__jruby_is_not_cat_friendly__".freeze : "@😸".freeze
|
|
26
|
+
|
|
27
|
+
def let_it_be(identifier, **options, &block)
|
|
28
|
+
initializer = proc do
|
|
29
|
+
instance_variable_set(:"#{PREFIX}#{identifier}", instance_exec(&block))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if within_before_all?
|
|
33
|
+
before(:all, &initializer)
|
|
34
|
+
else
|
|
35
|
+
before_all(&initializer)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
define_let_it_be_methods(identifier, **options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def define_let_it_be_methods(identifier, reload: false, refind: false)
|
|
42
|
+
let_accessor = -> { instance_variable_get(:"#{PREFIX}#{identifier}") }
|
|
43
|
+
|
|
44
|
+
if reload
|
|
45
|
+
let_accessor = lambda do
|
|
46
|
+
record = instance_variable_get(:"#{PREFIX}#{identifier}")
|
|
47
|
+
next unless record.is_a?(::ActiveRecord::Base)
|
|
48
|
+
record.reload
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if refind
|
|
53
|
+
let_accessor = lambda do
|
|
54
|
+
record = instance_variable_get(:"#{PREFIX}#{identifier}")
|
|
55
|
+
next unless record.is_a?(::ActiveRecord::Base)
|
|
56
|
+
|
|
57
|
+
record.class.find(record.send(record.class.primary_key))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
LetItBe.module_for(self).module_eval do
|
|
62
|
+
define_method(identifier) do
|
|
63
|
+
# Trying to detect the context (couldn't find other way so far)
|
|
64
|
+
if @__inspect_output =~ /\(:context\)/
|
|
65
|
+
instance_variable_get(:"#{PREFIX}#{identifier}")
|
|
66
|
+
else
|
|
67
|
+
# Fallback to let definition
|
|
68
|
+
super()
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
let(identifier, &let_accessor)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
RSpec.configure do |config|
|
|
79
|
+
config.extend TestProf::LetItBe
|
|
80
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_prof/ext/float_duration"
|
|
4
|
+
require "test_prof/ext/string_truncate"
|
|
5
|
+
require "test_prof/utils/sized_ordered_set"
|
|
6
|
+
require "test_prof/ext/string_strip_heredoc"
|
|
7
|
+
|
|
8
|
+
module TestProf
|
|
9
|
+
module RSpecDissect
|
|
10
|
+
class Listener # :nodoc:
|
|
11
|
+
include Logging
|
|
12
|
+
using FloatDuration
|
|
13
|
+
using StringTruncate
|
|
14
|
+
using StringStripHeredoc
|
|
15
|
+
|
|
16
|
+
NOTIFICATIONS = %i[
|
|
17
|
+
example_finished
|
|
18
|
+
example_group_finished
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@before_results = Utils::SizedOrderedSet.new(
|
|
23
|
+
top_count, sort_by: :before
|
|
24
|
+
)
|
|
25
|
+
@memo_results = Utils::SizedOrderedSet.new(
|
|
26
|
+
top_count, sort_by: :memo
|
|
27
|
+
)
|
|
28
|
+
@examples_count = 0
|
|
29
|
+
@examples_time = 0.0
|
|
30
|
+
@total_examples_time = 0.0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def example_finished(notification)
|
|
34
|
+
@examples_count += 1
|
|
35
|
+
@examples_time += notification.example.execution_result.run_time
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def example_group_finished(notification)
|
|
39
|
+
return unless notification.group.top_level?
|
|
40
|
+
|
|
41
|
+
data = {}
|
|
42
|
+
data[:total] = @examples_time
|
|
43
|
+
data[:count] = @examples_count
|
|
44
|
+
data[:before] = RSpecDissect.before_time
|
|
45
|
+
data[:memo] = RSpecDissect.memo_time
|
|
46
|
+
data[:desc] = notification.group.top_level_description
|
|
47
|
+
data[:loc] = notification.group.metadata[:location]
|
|
48
|
+
|
|
49
|
+
@before_results << data
|
|
50
|
+
@memo_results << data
|
|
51
|
+
|
|
52
|
+
@total_examples_time += @examples_time
|
|
53
|
+
@examples_count = 0
|
|
54
|
+
@examples_time = 0.0
|
|
55
|
+
|
|
56
|
+
RSpecDissect.reset!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def print
|
|
60
|
+
msgs = []
|
|
61
|
+
|
|
62
|
+
msgs <<
|
|
63
|
+
<<-MSG.strip_heredoc
|
|
64
|
+
RSpecDissect report
|
|
65
|
+
|
|
66
|
+
Total time: #{@total_examples_time.duration}
|
|
67
|
+
Total `before(:each)` time: #{RSpecDissect.total_before_time.duration}
|
|
68
|
+
Total `let` time: #{RSpecDissect.total_memo_time.duration}
|
|
69
|
+
|
|
70
|
+
MSG
|
|
71
|
+
|
|
72
|
+
msgs <<
|
|
73
|
+
<<-MSG.strip_heredoc
|
|
74
|
+
Top #{top_count} slowest suites (by `before(:each)` time):
|
|
75
|
+
|
|
76
|
+
MSG
|
|
77
|
+
|
|
78
|
+
@before_results.each do |group|
|
|
79
|
+
msgs <<
|
|
80
|
+
<<-GROUP.strip_heredoc
|
|
81
|
+
#{group[:desc].truncate} (#{group[:loc]}) – #{group[:before].duration} of #{group[:total].duration} (#{group[:count]})
|
|
82
|
+
GROUP
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
msgs <<
|
|
86
|
+
<<-MSG.strip_heredoc
|
|
87
|
+
|
|
88
|
+
Top #{top_count} slowest suites (by `let` time):
|
|
89
|
+
|
|
90
|
+
MSG
|
|
91
|
+
|
|
92
|
+
@memo_results.each do |group|
|
|
93
|
+
msgs <<
|
|
94
|
+
<<-GROUP.strip_heredoc
|
|
95
|
+
#{group[:desc].truncate} (#{group[:loc]}) – #{group[:memo].duration} of #{group[:total].duration} (#{group[:count]})
|
|
96
|
+
GROUP
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
log :info, msgs.join
|
|
100
|
+
|
|
101
|
+
stamp! if RSpecDissect.config.stamp?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def stamp!
|
|
105
|
+
stamper = RSpecStamp::Stamper.new
|
|
106
|
+
|
|
107
|
+
examples = Hash.new { |h, k| h[k] = [] }
|
|
108
|
+
|
|
109
|
+
(@before_results.to_a + @memo_results.to_a)
|
|
110
|
+
.map { |obj| obj[:loc] }.each do |location|
|
|
111
|
+
file, line = location.split(":")
|
|
112
|
+
examples[file] << line.to_i
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
examples.each do |file, lines|
|
|
116
|
+
stamper.stamp_file(file, lines.uniq)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
msgs = []
|
|
120
|
+
|
|
121
|
+
msgs <<
|
|
122
|
+
<<-MSG.strip_heredoc
|
|
123
|
+
RSpec Stamp results
|
|
124
|
+
|
|
125
|
+
Total patches: #{stamper.total}
|
|
126
|
+
Total files: #{examples.keys.size}
|
|
127
|
+
|
|
128
|
+
Failed patches: #{stamper.failed}
|
|
129
|
+
Ignored files: #{stamper.ignored}
|
|
130
|
+
MSG
|
|
131
|
+
|
|
132
|
+
log :info, msgs.join
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def top_count
|
|
138
|
+
RSpecDissect.config.top_count
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Register RSpecDissect listener
|
|
145
|
+
TestProf.activate('RD_PROF') do
|
|
146
|
+
RSpec.configure do |config|
|
|
147
|
+
listener = TestProf::RSpecDissect::Listener.new
|
|
148
|
+
|
|
149
|
+
config.before(:suite) do
|
|
150
|
+
config.reporter.register_listener(
|
|
151
|
+
listener, *TestProf::RSpecDissect::Listener::NOTIFICATIONS
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
config.after(:suite) { listener.print }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_prof/rspec_stamp"
|
|
4
|
+
require "test_prof/logging"
|
|
5
|
+
|
|
6
|
+
module TestProf
|
|
7
|
+
# RSpecDissect tracks how much time do you spend in `before` hooks
|
|
8
|
+
# and memoization helpers (i.e. `let`) in your tests.
|
|
9
|
+
module RSpecDissect
|
|
10
|
+
module ExampleInstrumentation # :nodoc:
|
|
11
|
+
def run_before_example(*)
|
|
12
|
+
RSpecDissect.track(:before) { super }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module MemoizedInstrumentation # :nodoc:
|
|
17
|
+
def fetch_or_store(*)
|
|
18
|
+
res = nil
|
|
19
|
+
Thread.current[:_rspec_dissect_memo_depth] ||= 0
|
|
20
|
+
Thread.current[:_rspec_dissect_memo_depth] += 1
|
|
21
|
+
begin
|
|
22
|
+
res = if Thread.current[:_rspec_dissect_memo_depth] == 1
|
|
23
|
+
RSpecDissect.track(:memo) { super }
|
|
24
|
+
else
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
ensure
|
|
28
|
+
Thread.current[:_rspec_dissect_memo_depth] -= 1
|
|
29
|
+
end
|
|
30
|
+
res
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# RSpecDisect configuration
|
|
35
|
+
class Configuration
|
|
36
|
+
attr_accessor :top_count
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@top_count = (ENV['RD_PROF_TOP'] || 5).to_i
|
|
40
|
+
@stamp = ENV['RD_PROF_STAMP']
|
|
41
|
+
|
|
42
|
+
RSpecStamp.config.tags = @stamp if stamp?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stamp?
|
|
46
|
+
!@stamp.nil?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
METRICS = %w[before memo].freeze
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
include Logging
|
|
54
|
+
|
|
55
|
+
def config
|
|
56
|
+
@config ||= Configuration.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def configure
|
|
60
|
+
yield config
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def init
|
|
64
|
+
RSpec::Core::Example.prepend(ExampleInstrumentation)
|
|
65
|
+
RSpec::Core::MemoizedHelpers::ThreadsafeMemoized.prepend(MemoizedInstrumentation)
|
|
66
|
+
RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized.prepend(MemoizedInstrumentation)
|
|
67
|
+
|
|
68
|
+
@data = {}
|
|
69
|
+
|
|
70
|
+
METRICS.each do |type|
|
|
71
|
+
@data["total_#{type}"] = 0.0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
reset!
|
|
75
|
+
|
|
76
|
+
log :info, "RSpecDissect enabled"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def track(type)
|
|
80
|
+
start = TestProf.now
|
|
81
|
+
res = yield
|
|
82
|
+
delta = (TestProf.now - start)
|
|
83
|
+
type = type.to_s
|
|
84
|
+
@data[type] += delta
|
|
85
|
+
@data["total_#{type}"] += delta
|
|
86
|
+
res
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reset!
|
|
90
|
+
METRICS.each do |type|
|
|
91
|
+
@data[type.to_s] = 0.0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
METRICS.each do |type|
|
|
96
|
+
define_method("#{type}_time") do
|
|
97
|
+
@data[type.to_s]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
define_method("total_#{type}_time") do
|
|
101
|
+
@data["total_#{type}"]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
require "test_prof/rspec_dissect/rspec" if defined?(RSpec::Core)
|
|
109
|
+
|
|
110
|
+
TestProf.activate('RD_PROF') do
|
|
111
|
+
TestProf::RSpecDissect.init
|
|
112
|
+
end
|
|
@@ -8,7 +8,7 @@ module TestProf
|
|
|
8
8
|
module Parser
|
|
9
9
|
# Contains the result of parsing
|
|
10
10
|
class Result
|
|
11
|
-
attr_accessor :fname, :desc
|
|
11
|
+
attr_accessor :fname, :desc, :desc_const
|
|
12
12
|
attr_reader :tags, :htags
|
|
13
13
|
|
|
14
14
|
def add_tag(v)
|
|
@@ -29,6 +29,7 @@ module TestProf
|
|
|
29
29
|
|
|
30
30
|
class << self
|
|
31
31
|
# rubocop: disable Metrics/CyclomaticComplexity
|
|
32
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
32
33
|
def parse(code)
|
|
33
34
|
sexp = Ripper.sexp(code)
|
|
34
35
|
return unless sexp
|
|
@@ -57,11 +58,17 @@ module TestProf
|
|
|
57
58
|
res = Result.new
|
|
58
59
|
|
|
59
60
|
fcall = sexp[1][0][1]
|
|
60
|
-
fcall = fcall[1] if fcall.first == :fcall
|
|
61
|
-
res.fname = fcall[1]
|
|
62
|
-
|
|
63
61
|
args_block = sexp[1][0][2]
|
|
64
62
|
|
|
63
|
+
if fcall.first == :fcall
|
|
64
|
+
fcall = fcall[1]
|
|
65
|
+
elsif fcall.first == :var_ref
|
|
66
|
+
res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
|
|
67
|
+
args_block = sexp[1][0][4]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
res.fname ||= fcall[1]
|
|
71
|
+
|
|
65
72
|
return res if args_block.nil?
|
|
66
73
|
|
|
67
74
|
args_block = args_block[1] if args_block.first == :arg_paren
|
|
@@ -70,6 +77,8 @@ module TestProf
|
|
|
70
77
|
|
|
71
78
|
if args.first.first == :string_literal
|
|
72
79
|
res.desc = parse_literal(args.shift)
|
|
80
|
+
elsif args.first.first == :var_ref || args.first.first == :const_path_ref
|
|
81
|
+
res.desc_const = parse_const(args.shift)
|
|
73
82
|
end
|
|
74
83
|
|
|
75
84
|
parse_arg(res, args.shift) until args.empty?
|
|
@@ -77,6 +86,7 @@ module TestProf
|
|
|
77
86
|
res
|
|
78
87
|
end
|
|
79
88
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
89
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
80
90
|
|
|
81
91
|
private
|
|
82
92
|
|
|
@@ -90,7 +100,24 @@ module TestProf
|
|
|
90
100
|
|
|
91
101
|
def parse_hash(res, hash_arg)
|
|
92
102
|
hash_arg.each do |(_, label, val)|
|
|
93
|
-
res.add_htag label[1][0..-2].to_sym,
|
|
103
|
+
res.add_htag label[1][0..-2].to_sym, parse_value(val)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Expr of the form:
|
|
108
|
+
# bool - [:var_ref, [:@kw, "true", [1, 24]]]
|
|
109
|
+
# string - [:string_literal, [:string_content, [...]]]
|
|
110
|
+
# int - [:@int, "3", [1, 52]]]]
|
|
111
|
+
def parse_value(expr)
|
|
112
|
+
case expr.first
|
|
113
|
+
when :var_ref
|
|
114
|
+
expr[1][1] == "true"
|
|
115
|
+
when :@int
|
|
116
|
+
expr[1].to_i
|
|
117
|
+
when :@float
|
|
118
|
+
expr[1].to_f
|
|
119
|
+
else
|
|
120
|
+
parse_literal(expr)
|
|
94
121
|
end
|
|
95
122
|
end
|
|
96
123
|
|
|
@@ -102,6 +129,25 @@ module TestProf
|
|
|
102
129
|
expr[0] == :assoc_new
|
|
103
130
|
val
|
|
104
131
|
end
|
|
132
|
+
|
|
133
|
+
# Expr of the form:
|
|
134
|
+
# [:var_ref, [:@const, "User", [1, 9]]]
|
|
135
|
+
#
|
|
136
|
+
# or
|
|
137
|
+
#
|
|
138
|
+
# [:const_path_ref, [:const_path_ref, [:var_ref,
|
|
139
|
+
# [:@const, "User", [1, 17]]],
|
|
140
|
+
# [:@const, "Guest", [1, 23]]],
|
|
141
|
+
# [:@const, "Collection", [1, 30]]
|
|
142
|
+
def parse_const(expr)
|
|
143
|
+
if expr.first == :var_ref
|
|
144
|
+
expr[1][1]
|
|
145
|
+
elsif expr.first == :@const
|
|
146
|
+
expr[1]
|
|
147
|
+
elsif expr.first == :const_path_ref
|
|
148
|
+
expr[1..-1].map(&method(:parse_const)).join("::")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
105
151
|
end
|
|
106
152
|
end
|
|
107
153
|
end
|
|
@@ -30,8 +30,10 @@ module TestProf
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def stamp!
|
|
33
|
+
stamper = Stamper.new
|
|
34
|
+
|
|
33
35
|
@examples.each do |file, lines|
|
|
34
|
-
stamp_file(file, lines.uniq)
|
|
36
|
+
stamper.stamp_file(file, lines.uniq)
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
msgs = []
|
|
@@ -40,44 +42,15 @@ module TestProf
|
|
|
40
42
|
<<-MSG.strip_heredoc
|
|
41
43
|
RSpec Stamp results
|
|
42
44
|
|
|
43
|
-
Total patches: #{
|
|
45
|
+
Total patches: #{stamper.total}
|
|
44
46
|
Total files: #{@examples.keys.size}
|
|
45
47
|
|
|
46
|
-
Failed patches: #{
|
|
47
|
-
Ignored files: #{
|
|
48
|
+
Failed patches: #{stamper.failed}
|
|
49
|
+
Ignored files: #{stamper.ignored}
|
|
48
50
|
MSG
|
|
49
51
|
|
|
50
52
|
log :info, msgs.join
|
|
51
53
|
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def stamp_file(file, lines)
|
|
56
|
-
@total += lines.size
|
|
57
|
-
return if ignored?(file)
|
|
58
|
-
|
|
59
|
-
log :info, "(dry-run) Patching #{file}" if dry_run?
|
|
60
|
-
|
|
61
|
-
code = File.readlines(file)
|
|
62
|
-
|
|
63
|
-
@failed += RSpecStamp.apply_tags(code, lines, RSpecStamp.config.tags)
|
|
64
|
-
|
|
65
|
-
File.write(file, code.join) unless dry_run?
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def ignored?(file)
|
|
69
|
-
ignored = RSpecStamp.config.ignore_files.find do |pattern|
|
|
70
|
-
file =~ pattern
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
return unless ignored
|
|
74
|
-
log :warn, "Ignore stamping file: #{file}"
|
|
75
|
-
@ignored += 1
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def dry_run?
|
|
79
|
-
RSpecStamp.config.dry_run?
|
|
80
|
-
end
|
|
81
54
|
end
|
|
82
55
|
end
|
|
83
56
|
end
|
|
@@ -10,7 +10,8 @@ module TestProf
|
|
|
10
10
|
|
|
11
11
|
# RSpecStamp configuration
|
|
12
12
|
class Configuration
|
|
13
|
-
|
|
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
|
|
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.
|
|
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
|
-
"#{[
|
|
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
|
data/lib/test_prof/ruby_prof.rb
CHANGED
|
@@ -50,7 +50,8 @@ module TestProf
|
|
|
50
50
|
:include_threads, :eliminate_methods
|
|
51
51
|
|
|
52
52
|
def initialize
|
|
53
|
-
@printer = ENV.
|
|
53
|
+
@printer = ENV['TEST_RUBY_PROF'].to_sym if PRINTERS.key?(ENV['TEST_RUBY_PROF'])
|
|
54
|
+
@printer ||= ENV.fetch('TEST_RUBY_PROF_PRINTER', :flat).to_sym
|
|
54
55
|
@mode = ENV.fetch('TEST_RUBY_PROF_MODE', :wall).to_sym
|
|
55
56
|
@min_percent = 1
|
|
56
57
|
@include_threads = false
|
data/lib/test_prof/stack_prof.rb
CHANGED
|
@@ -42,7 +42,11 @@ module TestProf
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def each
|
|
45
|
-
|
|
45
|
+
if block_given?
|
|
46
|
+
data.each(&Proc.new)
|
|
47
|
+
else
|
|
48
|
+
data.each
|
|
49
|
+
end
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def size
|
|
@@ -50,7 +54,7 @@ module TestProf
|
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def to_a
|
|
53
|
-
data
|
|
57
|
+
data.dup
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
private
|
data/lib/test_prof/version.rb
CHANGED
data/lib/test_prof.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
|
|
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-
|
|
11
|
+
date: 2017-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -72,14 +72,14 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0.
|
|
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.
|
|
82
|
+
version: '0.50'
|
|
83
83
|
description: "\n Ruby applications tests profiling tools.\n\n Contains tools
|
|
84
84
|
to anylyze factories usage, integrate with Ruby profilers,\n profile your examples
|
|
85
85
|
using ActiveSupport notifications (if any) and\n statically analyze your code
|
|
@@ -108,6 +108,8 @@ files:
|
|
|
108
108
|
- guides/factory_default.md
|
|
109
109
|
- guides/factory_doctor.md
|
|
110
110
|
- guides/factory_prof.md
|
|
111
|
+
- guides/let_it_be.md
|
|
112
|
+
- guides/rspec_dissect.md
|
|
111
113
|
- guides/rspec_stamp.md
|
|
112
114
|
- guides/rubocop.md
|
|
113
115
|
- guides/ruby_prof.md
|
|
@@ -145,7 +147,10 @@ files:
|
|
|
145
147
|
- lib/test_prof/recipes/rspec/any_fixture.rb
|
|
146
148
|
- lib/test_prof/recipes/rspec/before_all.rb
|
|
147
149
|
- lib/test_prof/recipes/rspec/factory_default.rb
|
|
150
|
+
- lib/test_prof/recipes/rspec/let_it_be.rb
|
|
148
151
|
- lib/test_prof/recipes/rspec/sample.rb
|
|
152
|
+
- lib/test_prof/rspec_dissect.rb
|
|
153
|
+
- lib/test_prof/rspec_dissect/rspec.rb
|
|
149
154
|
- lib/test_prof/rspec_stamp.rb
|
|
150
155
|
- lib/test_prof/rspec_stamp/parser.rb
|
|
151
156
|
- lib/test_prof/rspec_stamp/rspec.rb
|
|
@@ -179,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
179
184
|
version: '0'
|
|
180
185
|
requirements: []
|
|
181
186
|
rubyforge_project:
|
|
182
|
-
rubygems_version: 2.6.
|
|
187
|
+
rubygems_version: 2.6.11
|
|
183
188
|
signing_key:
|
|
184
189
|
specification_version: 4
|
|
185
190
|
summary: Ruby applications tests profiling tools
|