test-prof 0.1.0.beta1
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 +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
|