test-prof 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +103 -0
  5. data/assets/flamegraph.demo.html +173 -0
  6. data/assets/flamegraph.template.html +196 -0
  7. data/assets/src/d3-tip.js +352 -0
  8. data/assets/src/d3-tip.min.js +1 -0
  9. data/assets/src/d3.flameGraph.css +92 -0
  10. data/assets/src/d3.flameGraph.js +459 -0
  11. data/assets/src/d3.flameGraph.min.css +1 -0
  12. data/assets/src/d3.flameGraph.min.js +1 -0
  13. data/assets/src/d3.v4.min.js +8 -0
  14. data/guides/any_fixture.md +60 -0
  15. data/guides/before_all.md +98 -0
  16. data/guides/event_prof.md +97 -0
  17. data/guides/factory_doctor.md +64 -0
  18. data/guides/factory_prof.md +85 -0
  19. data/guides/rspec_stamp.md +53 -0
  20. data/guides/rubocop.md +48 -0
  21. data/guides/ruby_prof.md +61 -0
  22. data/guides/stack_prof.md +43 -0
  23. data/lib/test-prof.rb +3 -0
  24. data/lib/test_prof/any_fixture.rb +67 -0
  25. data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
  26. data/lib/test_prof/event_prof/custom_events/factory_create.rb +51 -0
  27. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +48 -0
  28. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +38 -0
  29. data/lib/test_prof/event_prof/custom_events.rb +5 -0
  30. data/lib/test_prof/event_prof/instrumentations/active_support.rb +16 -0
  31. data/lib/test_prof/event_prof/minitest.rb +3 -0
  32. data/lib/test_prof/event_prof/rspec.rb +94 -0
  33. data/lib/test_prof/event_prof.rb +177 -0
  34. data/lib/test_prof/ext/float_duration.rb +14 -0
  35. data/lib/test_prof/factory_doctor/factory_girl_patch.rb +12 -0
  36. data/lib/test_prof/factory_doctor/minitest.rb +3 -0
  37. data/lib/test_prof/factory_doctor/rspec.rb +96 -0
  38. data/lib/test_prof/factory_doctor.rb +133 -0
  39. data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
  40. data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
  41. data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
  42. data/lib/test_prof/factory_prof.rb +140 -0
  43. data/lib/test_prof/logging.rb +25 -0
  44. data/lib/test_prof/recipes/rspec/any_fixture.rb +21 -0
  45. data/lib/test_prof/recipes/rspec/before_all.rb +23 -0
  46. data/lib/test_prof/rspec_stamp/parser.rb +103 -0
  47. data/lib/test_prof/rspec_stamp/rspec.rb +91 -0
  48. data/lib/test_prof/rspec_stamp.rb +135 -0
  49. data/lib/test_prof/rubocop.rb +3 -0
  50. data/lib/test_prof/ruby_prof/rspec.rb +13 -0
  51. data/lib/test_prof/ruby_prof.rb +194 -0
  52. data/lib/test_prof/stack_prof/rspec.rb +13 -0
  53. data/lib/test_prof/stack_prof.rb +120 -0
  54. data/lib/test_prof/version.rb +5 -0
  55. data/lib/test_prof.rb +108 -0
  56. 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`).
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof"
@@ -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