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.
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