test-prof 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/assets/flamegraph.demo.html +173 -0
- data/assets/flamegraph.template.html +196 -0
- data/assets/src/d3-tip.js +352 -0
- data/assets/src/d3-tip.min.js +1 -0
- data/assets/src/d3.flameGraph.css +92 -0
- data/assets/src/d3.flameGraph.js +459 -0
- data/assets/src/d3.flameGraph.min.css +1 -0
- data/assets/src/d3.flameGraph.min.js +1 -0
- data/assets/src/d3.v4.min.js +8 -0
- data/guides/any_fixture.md +60 -0
- data/guides/before_all.md +98 -0
- data/guides/event_prof.md +97 -0
- data/guides/factory_doctor.md +64 -0
- data/guides/factory_prof.md +85 -0
- data/guides/rspec_stamp.md +53 -0
- data/guides/rubocop.md +48 -0
- data/guides/ruby_prof.md +61 -0
- data/guides/stack_prof.md +43 -0
- data/lib/test-prof.rb +3 -0
- data/lib/test_prof/any_fixture.rb +67 -0
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +51 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +48 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +38 -0
- data/lib/test_prof/event_prof/custom_events.rb +5 -0
- data/lib/test_prof/event_prof/instrumentations/active_support.rb +16 -0
- data/lib/test_prof/event_prof/minitest.rb +3 -0
- data/lib/test_prof/event_prof/rspec.rb +94 -0
- data/lib/test_prof/event_prof.rb +177 -0
- data/lib/test_prof/ext/float_duration.rb +14 -0
- data/lib/test_prof/factory_doctor/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_doctor/minitest.rb +3 -0
- data/lib/test_prof/factory_doctor/rspec.rb +96 -0
- data/lib/test_prof/factory_doctor.rb +133 -0
- data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
- data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
- data/lib/test_prof/factory_prof.rb +140 -0
- data/lib/test_prof/logging.rb +25 -0
- data/lib/test_prof/recipes/rspec/any_fixture.rb +21 -0
- data/lib/test_prof/recipes/rspec/before_all.rb +23 -0
- data/lib/test_prof/rspec_stamp/parser.rb +103 -0
- data/lib/test_prof/rspec_stamp/rspec.rb +91 -0
- data/lib/test_prof/rspec_stamp.rb +135 -0
- data/lib/test_prof/rubocop.rb +3 -0
- data/lib/test_prof/ruby_prof/rspec.rb +13 -0
- data/lib/test_prof/ruby_prof.rb +194 -0
- data/lib/test_prof/stack_prof/rspec.rb +13 -0
- data/lib/test_prof/stack_prof.rb +120 -0
- data/lib/test_prof/version.rb +5 -0
- data/lib/test_prof.rb +108 -0
- metadata +227 -0
data/guides/rubocop.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Custom Rubocop Cops
|
2
|
+
|
3
|
+
TestProf comes with the [Rubocop](https://github.com/bbatsov/rubocop) cops that help you write more performant tests.
|
4
|
+
|
5
|
+
To enable them:
|
6
|
+
|
7
|
+
- Require `test_prof/rubocop` in your Rubocop configuration:
|
8
|
+
|
9
|
+
```yml
|
10
|
+
# .rubocop.yml
|
11
|
+
require:
|
12
|
+
- 'test_prof/rubocop'
|
13
|
+
```
|
14
|
+
|
15
|
+
- Enable cops:
|
16
|
+
|
17
|
+
```yml
|
18
|
+
RSpec/AggregateFailures:
|
19
|
+
Enabled: true
|
20
|
+
Include:
|
21
|
+
- 'spec/**/*.rb'
|
22
|
+
```
|
23
|
+
|
24
|
+
## Rspec/AggregateFailures
|
25
|
+
|
26
|
+
This cop encourages you to use one of the greatest features of the recent RSpec – aggregating failures within an example.
|
27
|
+
|
28
|
+
Instead of writing one example per assertion, you can group _independent_ assertions together, thus running all setup hooks only once. That can dramatically increase your performance (by reducing the total number of examples).
|
29
|
+
|
30
|
+
Consider an example:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# bad
|
34
|
+
it { is_expected.to be_success }
|
35
|
+
it { is_expected.to have_header('X-TOTAL-PAGES', 10) }
|
36
|
+
it { is_expected.to have_header('X-NEXT-PAGE', 2) }
|
37
|
+
|
38
|
+
# good
|
39
|
+
it "returns the second page", :aggregate_failures do
|
40
|
+
is_expected.to be_success
|
41
|
+
is_expected.to have_header('X-TOTAL-PAGES', 10)
|
42
|
+
is_expected.to have_header('X-NEXT-PAGE', 2)
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
This cop supports auto-correct feature, so you can automatically refactor you legacy tests!
|
47
|
+
|
48
|
+
**NOTE**: auto-correction may break your tests (especially the ones using block-matchers, such as `change`).
|
data/guides/ruby_prof.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Profiling with RubyProf
|
2
|
+
|
3
|
+
Easily integrate the power of [ruby-prof](https://github.com/ruby-prof/ruby-prof) into your test suite.
|
4
|
+
|
5
|
+
## Instructions
|
6
|
+
|
7
|
+
Install `ruby-prof` gem (>= 0.16):
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Gemfile
|
11
|
+
group :development, :test do
|
12
|
+
gem 'ruby-prof', require: false
|
13
|
+
end
|
14
|
+
```
|
15
|
+
|
16
|
+
RubyProf profiler has two modes: _global_ and _per-example_.
|
17
|
+
|
18
|
+
You can activate the global profiling using the environment variable `TEST_RUBY_PROF`:
|
19
|
+
|
20
|
+
```sh
|
21
|
+
TEST_RUBY_PROF=1 bundle exec rake test
|
22
|
+
|
23
|
+
# or for RSpec
|
24
|
+
TEST_RUBY_PROF=1 rspec ...
|
25
|
+
```
|
26
|
+
|
27
|
+
Or in your code:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
TestProf::RubyProf.run
|
31
|
+
```
|
32
|
+
|
33
|
+
TestProf provides a built-in shared context for RSpec to profile examples individually:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
it "is doing heavy stuff", :rprof do
|
37
|
+
...
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
### Configuration
|
42
|
+
|
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
|
+
|
45
|
+
You can specify a printer through environment variable `TEST_RUBY_PROF_PRINTER`:
|
46
|
+
|
47
|
+
```sh
|
48
|
+
TEST_RUBY_PROF_PRINTER=flat bundle exec rake test
|
49
|
+
```
|
50
|
+
|
51
|
+
Or in your code:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
TestProf::RubyProf.configure do |config|
|
55
|
+
config.printer = :flat
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
By default, we use `CallStackPrinter`.
|
60
|
+
|
61
|
+
See [ruby_prof.rb](https://github.com/palkan/test-prof/tree/master/lib/test_prof/ruby_prof.rb) for all available configuration options and their usage.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Profiling with StackProf
|
2
|
+
|
3
|
+
[StackProf](https://github.com/tmm1/stackprof) is a sampling call-stack profiler for ruby.
|
4
|
+
|
5
|
+
## Instructions
|
6
|
+
|
7
|
+
Install 'stackprof' gem (>= 0.2.7):
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Gemfile
|
11
|
+
group :development, :test do
|
12
|
+
gem 'stackprof', require: false
|
13
|
+
end
|
14
|
+
```
|
15
|
+
|
16
|
+
StackProf profiler has 2 modes: _global_ and _per-example_.
|
17
|
+
|
18
|
+
You can activate the global profiling using the environment variable `TEST_STACK_PROF`:
|
19
|
+
|
20
|
+
```sh
|
21
|
+
TEST_STACK_PROF=1 bundle exec rake test
|
22
|
+
|
23
|
+
# or for RSpec
|
24
|
+
TEST_STACK_PROF=1 rspec ...
|
25
|
+
```
|
26
|
+
|
27
|
+
Or in your code:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
TestProf::StackProf.run
|
31
|
+
```
|
32
|
+
|
33
|
+
TestProf provides a built-in shared context for RSpec to profile examples individually:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
it "is doing heavy stuff", :sprof do
|
37
|
+
...
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
### Configuration
|
42
|
+
|
43
|
+
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.
|
data/lib/test-prof.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
# Make DB fixtures from blocks.
|
5
|
+
module AnyFixture
|
6
|
+
INSERT_RXP = /^INSERT INTO ([\S]+)/
|
7
|
+
|
8
|
+
class Cache # :nodoc:
|
9
|
+
attr_reader :store
|
10
|
+
|
11
|
+
delegate :clear, to: :store
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@store = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(key)
|
18
|
+
return store[key] if store.key?(key)
|
19
|
+
return unless block_given?
|
20
|
+
store[key] = yield
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Register a block of code as a fixture,
|
26
|
+
# returns the result of the block execution
|
27
|
+
def register(id)
|
28
|
+
cache.fetch(id) do
|
29
|
+
ActiveSupport::Notifications.subscribed(method(:subscriber), "sql.active_record") do
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Clean all affected tables (but do not reset cache)
|
36
|
+
def clean
|
37
|
+
tables_cache.keys.each do |table|
|
38
|
+
ActiveRecord::Base.connection.execute %(
|
39
|
+
DELETE FROM #{table}
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Reset all information and clean tables
|
45
|
+
def reset
|
46
|
+
clean
|
47
|
+
tables_cache.clear
|
48
|
+
cache.clear
|
49
|
+
end
|
50
|
+
|
51
|
+
def subscriber(_event, _start, _finish, _id, data)
|
52
|
+
matches = data.fetch(:sql).match(INSERT_RXP)
|
53
|
+
tables_cache[matches[1]] = true if matches
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def cache
|
59
|
+
@cache ||= Cache.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def tables_cache
|
63
|
+
@tables_cache ||= {}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubocop'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module RSpec
|
8
|
+
# Rejects and auto-corrects the usage of one-liners examples in favour of
|
9
|
+
# :aggregate_failures feature.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
# it { is_expected.to be_success }
|
15
|
+
# it { is_expected.to have_header('X-TOTAL-PAGES', 10) }
|
16
|
+
# it { is_expected.to have_header('X-NEXT-PAGE', 2) }
|
17
|
+
#
|
18
|
+
# # good
|
19
|
+
# it "returns the second page", :aggregate_failures do
|
20
|
+
# is_expected.to be_success
|
21
|
+
# is_expected.to have_header('X-TOTAL-PAGES', 10)
|
22
|
+
# is_expected.to have_header('X-NEXT-PAGE', 2)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
class AggregateFailures < RuboCop::Cop::Cop
|
26
|
+
# From https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/language.rb
|
27
|
+
GROUP_BLOCKS = %i[
|
28
|
+
describe context feature example_group
|
29
|
+
xdescribe xcontext xfeature
|
30
|
+
fdescribe fcontext ffeature
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
EXAMPLE_BLOCKS = %i[
|
34
|
+
it specify example scenario its
|
35
|
+
fit fspecify fexample fscenario focus
|
36
|
+
xit xspecify xexample xscenario ski
|
37
|
+
pending
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
def on_block(node)
|
41
|
+
method, _args, body = *node
|
42
|
+
return unless body&.begin_type?
|
43
|
+
|
44
|
+
_receiver, method_name, _object = *method
|
45
|
+
return unless GROUP_BLOCKS.include?(method_name)
|
46
|
+
|
47
|
+
return if check_node(body)
|
48
|
+
|
49
|
+
add_offense(
|
50
|
+
node,
|
51
|
+
:expression,
|
52
|
+
'Use :aggregate_failures instead of several one-liners.'
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def autocorrect(node)
|
57
|
+
_method, _args, body = *node
|
58
|
+
iter = body.children.each
|
59
|
+
|
60
|
+
first_example = loop do
|
61
|
+
child = iter.next
|
62
|
+
break child if oneliner?(child)
|
63
|
+
end
|
64
|
+
|
65
|
+
base_indent = " " * first_example.source_range.column
|
66
|
+
|
67
|
+
replacements = [
|
68
|
+
header_from(first_example),
|
69
|
+
body_from(first_example, base_indent)
|
70
|
+
]
|
71
|
+
|
72
|
+
last_example = nil
|
73
|
+
|
74
|
+
loop do
|
75
|
+
child = iter.next
|
76
|
+
break unless oneliner?(child)
|
77
|
+
last_example = child
|
78
|
+
replacements << body_from(child, base_indent)
|
79
|
+
end
|
80
|
+
|
81
|
+
replacements << "#{base_indent}end"
|
82
|
+
|
83
|
+
range = first_example.source_range.begin.join(
|
84
|
+
last_example.source_range.end
|
85
|
+
)
|
86
|
+
|
87
|
+
replacement = replacements.join("\n")
|
88
|
+
|
89
|
+
lambda do |corrector|
|
90
|
+
corrector.replace(range, replacement)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def check_node(node)
|
97
|
+
offenders = 0
|
98
|
+
|
99
|
+
node.children.each do |child|
|
100
|
+
if oneliner?(child)
|
101
|
+
offenders += 1
|
102
|
+
elsif example_node?(child)
|
103
|
+
break if offenders > 1
|
104
|
+
offenders = 0
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
offenders < 2
|
109
|
+
end
|
110
|
+
|
111
|
+
def oneliner?(node)
|
112
|
+
node&.block_type? &&
|
113
|
+
(node.source.lines.size == 1) &&
|
114
|
+
example_node?(node)
|
115
|
+
end
|
116
|
+
|
117
|
+
def example_node?(node)
|
118
|
+
method, _args, _body = *node
|
119
|
+
_receiver, method_name, _object = *method
|
120
|
+
EXAMPLE_BLOCKS.include?(method_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
def header_from(node)
|
124
|
+
method, _args, _body = *node
|
125
|
+
_receiver, method_name, _object = *method
|
126
|
+
%(#{method_name} "works", :aggregate_failures do)
|
127
|
+
end
|
128
|
+
|
129
|
+
def body_from(node, base_indent = '')
|
130
|
+
_method, _args, body = *node
|
131
|
+
"#{base_indent}#{indent}#{body.source}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def indent
|
135
|
+
@indent ||= " " * (config.for_cop('IndentationWidth')['Width'] || 2)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::EventProf::CustomEvents
|
4
|
+
module FactoryCreate # :nodoc: all
|
5
|
+
module RunnerPatch
|
6
|
+
def run(strategy = @strategy)
|
7
|
+
return super unless strategy == :create
|
8
|
+
FactoryCreate.track(@name) do
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def setup!
|
16
|
+
@depth = 0
|
17
|
+
FactoryGirl::FactoryRunner.prepend RunnerPatch
|
18
|
+
end
|
19
|
+
|
20
|
+
def track(factory)
|
21
|
+
@depth += 1
|
22
|
+
res = nil
|
23
|
+
begin
|
24
|
+
res =
|
25
|
+
if @depth == 1
|
26
|
+
ActiveSupport::Notifications.instrument(
|
27
|
+
'factory.create',
|
28
|
+
name: factory
|
29
|
+
) { yield }
|
30
|
+
else
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
ensure
|
34
|
+
@depth -= 1
|
35
|
+
end
|
36
|
+
res
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if TestProf.require(
|
43
|
+
'factory_girl',
|
44
|
+
<<~MSG
|
45
|
+
Failed to load FactoryGirl.
|
46
|
+
|
47
|
+
Make sure that "factory_girl" gem is in your Gemfile.
|
48
|
+
MSG
|
49
|
+
)
|
50
|
+
TestProf::EventProf::CustomEvents::FactoryCreate.setup!
|
51
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::EventProf::CustomEvents
|
4
|
+
module SidekiqInline # :nodoc: all
|
5
|
+
module ClientPatch
|
6
|
+
def raw_push(*)
|
7
|
+
return super unless Sidekiq::Testing.inline?
|
8
|
+
SidekiqInline.track { super }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def setup!
|
14
|
+
@depth = 0
|
15
|
+
Sidekiq::Client.prepend ClientPatch
|
16
|
+
end
|
17
|
+
|
18
|
+
def track
|
19
|
+
@depth += 1
|
20
|
+
res = nil
|
21
|
+
begin
|
22
|
+
res =
|
23
|
+
if @depth == 1
|
24
|
+
ActiveSupport::Notifications.instrument(
|
25
|
+
'sidekiq.inline'
|
26
|
+
) { yield }
|
27
|
+
else
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
@depth -= 1
|
32
|
+
end
|
33
|
+
res
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if TestProf.require(
|
40
|
+
'sidekiq/testing',
|
41
|
+
<<~MSG
|
42
|
+
Failed to load Sidekiq.
|
43
|
+
|
44
|
+
Make sure that "sidekiq" gem is in your Gemfile.
|
45
|
+
MSG
|
46
|
+
)
|
47
|
+
TestProf::EventProf::CustomEvents::SidekiqInline.setup!
|
48
|
+
end
|