test-prof 0.1.0.pre5 → 0.1.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +16 -4
  4. data/assets/flamegraph.demo.html +173 -0
  5. data/assets/flamegraph.template.html +196 -0
  6. data/assets/src/d3-tip.js +352 -0
  7. data/assets/src/d3-tip.min.js +1 -0
  8. data/assets/src/d3.flameGraph.css +92 -0
  9. data/assets/src/d3.flameGraph.js +459 -0
  10. data/assets/src/d3.flameGraph.min.css +1 -0
  11. data/assets/src/d3.flameGraph.min.js +1 -0
  12. data/assets/src/d3.v4.min.js +8 -0
  13. data/guides/any_fixture.md +1 -1
  14. data/guides/event_prof.md +30 -0
  15. data/guides/factory_default.md +109 -0
  16. data/guides/factory_prof.md +85 -0
  17. data/guides/rubocop.md +48 -0
  18. data/guides/ruby_prof.md +2 -0
  19. data/guides/stack_prof.md +5 -1
  20. data/guides/tag_prof.md +52 -0
  21. data/guides/tests_sampling.md +24 -0
  22. data/lib/test_prof.rb +31 -7
  23. data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
  24. data/lib/test_prof/event_prof/custom_events.rb +3 -3
  25. data/lib/test_prof/event_prof/custom_events/factory_create.rb +10 -8
  26. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +10 -8
  27. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +12 -10
  28. data/lib/test_prof/event_prof/rspec.rb +5 -1
  29. data/lib/test_prof/factory_default.rb +58 -0
  30. data/lib/test_prof/factory_default/factory_girl_patch.rb +22 -0
  31. data/lib/test_prof/factory_doctor.rb +11 -9
  32. data/lib/test_prof/factory_doctor/rspec.rb +5 -3
  33. data/lib/test_prof/factory_prof.rb +140 -0
  34. data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
  35. data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
  36. data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
  37. data/lib/test_prof/recipes/minitest/sample.rb +29 -0
  38. data/lib/test_prof/recipes/rspec/factory_default.rb +9 -0
  39. data/lib/test_prof/recipes/rspec/sample.rb +13 -0
  40. data/lib/test_prof/rspec_stamp/rspec.rb +5 -1
  41. data/lib/test_prof/rubocop.rb +3 -0
  42. data/lib/test_prof/ruby_prof.rb +6 -12
  43. data/lib/test_prof/stack_prof.rb +14 -7
  44. data/lib/test_prof/tag_prof.rb +8 -0
  45. data/lib/test_prof/tag_prof/rspec.rb +84 -0
  46. data/lib/test_prof/version.rb +1 -1
  47. metadata +48 -41
  48. data/.gitignore +0 -10
  49. data/.rspec +0 -2
  50. data/.rubocop.yml +0 -69
  51. data/.travis.yml +0 -5
  52. data/Gemfile +0 -4
  53. data/Rakefile +0 -8
  54. data/bin/setup +0 -8
  55. data/circle.yml +0 -11
  56. data/spec/integrations/any_fixture_spec.rb +0 -11
  57. data/spec/integrations/before_all_spec.rb +0 -11
  58. data/spec/integrations/event_prof_spec.rb +0 -100
  59. data/spec/integrations/factory_doctor_spec.rb +0 -20
  60. data/spec/integrations/fixtures/rspec/any_fixture_fixture.rb +0 -37
  61. data/spec/integrations/fixtures/rspec/before_all_fixture.rb +0 -32
  62. data/spec/integrations/fixtures/rspec/event_prof_factory_create_fixture.rb +0 -23
  63. data/spec/integrations/fixtures/rspec/event_prof_fixture.rb +0 -51
  64. data/spec/integrations/fixtures/rspec/event_prof_sidekiq_fixture.rb +0 -53
  65. data/spec/integrations/fixtures/rspec/factory_doctor_fixture.rb +0 -33
  66. data/spec/integrations/fixtures/rspec/rspec_stamp_fixture_tmpl.rb +0 -33
  67. data/spec/integrations/rspec_stamp_spec.rb +0 -53
  68. data/spec/spec_helper.rb +0 -38
  69. data/spec/support/ar_models.rb +0 -43
  70. data/spec/support/instrumenter_stub.rb +0 -19
  71. data/spec/support/integration_helpers.rb +0 -13
  72. data/spec/support/transactional_context.rb +0 -11
  73. data/spec/test_prof/any_fixture_spec.rb +0 -66
  74. data/spec/test_prof/event_prof_spec.rb +0 -138
  75. data/spec/test_prof/ext/float_duration_spec.rb +0 -12
  76. data/spec/test_prof/factory_doctor_spec.rb +0 -84
  77. data/spec/test_prof/rspec_stamp/parser_spec.rb +0 -58
  78. data/spec/test_prof/rspec_stamp_spec.rb +0 -281
  79. data/spec/test_prof/ruby_prof_spec.rb +0 -109
  80. data/spec/test_prof/stack_prof_spec.rb +0 -73
  81. data/spec/test_prof_spec.rb +0 -23
  82. data/test-prof.gemspec +0 -35
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/event_prof/custom_events/factory_create" if ENV['EVENT_PROF'] == "factory.create"
4
- require "test_prof/event_prof/custom_events/sidekiq_inline" if ENV['EVENT_PROF'] == "sidekiq.inline"
5
- require "test_prof/event_prof/custom_events/sidekiq_jobs" if ENV['EVENT_PROF'] == "sidekiq.jobs"
3
+ require "test_prof/event_prof/custom_events/factory_create"
4
+ require "test_prof/event_prof/custom_events/sidekiq_inline"
5
+ require "test_prof/event_prof/custom_events/sidekiq_jobs"
@@ -39,13 +39,15 @@ module TestProf::EventProf::CustomEvents
39
39
  end
40
40
  end
41
41
 
42
- if TestProf.require(
43
- 'factory_girl',
44
- <<~MSG
45
- Failed to load FactoryGirl.
42
+ TestProf.activate('EVENT_PROF', 'factory.create') do
43
+ if TestProf.require(
44
+ 'factory_girl',
45
+ <<~MSG
46
+ Failed to load FactoryGirl.
46
47
 
47
- Make sure that "factory_girl" gem is in your Gemfile.
48
- MSG
49
- )
50
- TestProf::EventProf::CustomEvents::FactoryCreate.setup!
48
+ Make sure that "factory_girl" gem is in your Gemfile.
49
+ MSG
50
+ )
51
+ TestProf::EventProf::CustomEvents::FactoryCreate.setup!
52
+ end
51
53
  end
@@ -36,13 +36,15 @@ module TestProf::EventProf::CustomEvents
36
36
  end
37
37
  end
38
38
 
39
- if TestProf.require(
40
- 'sidekiq/testing',
41
- <<~MSG
42
- Failed to load Sidekiq.
39
+ TestProf.activate('EVENT_PROF', 'sidekiq.inline') do
40
+ if TestProf.require(
41
+ 'sidekiq/testing',
42
+ <<~MSG
43
+ Failed to load Sidekiq.
43
44
 
44
- Make sure that "sidekiq" gem is in your Gemfile.
45
- MSG
46
- )
47
- TestProf::EventProf::CustomEvents::SidekiqInline.setup!
45
+ Make sure that "sidekiq" gem is in your Gemfile.
46
+ MSG
47
+ )
48
+ TestProf::EventProf::CustomEvents::SidekiqInline.setup!
49
+ end
48
50
  end
@@ -23,16 +23,18 @@ module TestProf::EventProf::CustomEvents
23
23
  end
24
24
  end
25
25
 
26
- if TestProf.require(
27
- 'sidekiq/testing',
28
- <<~MSG
29
- Failed to load Sidekiq.
26
+ TestProf.activate('EVENT_PROF', 'sidekiq.jobs') do
27
+ if TestProf.require(
28
+ 'sidekiq/testing',
29
+ <<~MSG
30
+ Failed to load Sidekiq.
30
31
 
31
- Make sure that "sidekiq" gem is in your Gemfile.
32
- MSG
33
- )
34
- TestProf::EventProf::CustomEvents::SidekiqJobs.setup!
35
- TestProf::EventProf.configure do |config|
36
- config.rank_by = :count
32
+ Make sure that "sidekiq" gem is in your Gemfile.
33
+ MSG
34
+ )
35
+ TestProf::EventProf::CustomEvents::SidekiqJobs.setup!
36
+ TestProf::EventProf.configure do |config|
37
+ config.rank_by = :count
38
+ end
37
39
  end
38
40
  end
@@ -87,7 +87,11 @@ TestProf.activate('EVENT_PROF') do
87
87
  RSpec.configure do |config|
88
88
  listener = TestProf::EventProf::RSpecListener.new
89
89
 
90
- config.reporter.register_listener(listener, *TestProf::EventProf::RSpecListener::NOTIFICATIONS)
90
+ config.before(:suite) do
91
+ config.reporter.register_listener(
92
+ listener, *TestProf::EventProf::RSpecListener::NOTIFICATIONS
93
+ )
94
+ end
91
95
 
92
96
  config.after(:suite) { listener.print }
93
97
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/factory_default/factory_girl_patch"
4
+
5
+ module TestProf
6
+ # FactoryDefault allows use to re-use associated objects
7
+ # in factories implicilty
8
+ module FactoryDefault
9
+ module DefaultSyntax # :nodoc:
10
+ def create_default(name, *args, &block)
11
+ set_factory_default(
12
+ name,
13
+ FactoryGirl.create(name, *args, &block)
14
+ )
15
+ end
16
+
17
+ def set_factory_default(name, obj)
18
+ FactoryDefault.register(name, obj)
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def init
24
+ FactoryGirl::Syntax::Methods.include DefaultSyntax
25
+ FactoryGirl.extend DefaultSyntax
26
+ FactoryGirl::Strategy::Create.prepend StrategyExt
27
+ FactoryGirl::Strategy::Build.prepend StrategyExt
28
+ FactoryGirl::Strategy::Stub.prepend StrategyExt
29
+
30
+ @store = {}
31
+ end
32
+
33
+ def register(name, obj)
34
+ store[name] = obj
35
+ end
36
+
37
+ def get(name)
38
+ store[name]
39
+ end
40
+
41
+ def exists?(name)
42
+ store.key?(name)
43
+ end
44
+
45
+ def remove(name)
46
+ store.delete(name)
47
+ end
48
+
49
+ def reset
50
+ @store.clear
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :store
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module FactoryDefault # :nodoc: all
5
+ module RunnerExt
6
+ refine FactoryGirl::FactoryRunner do
7
+ def name
8
+ @name
9
+ end
10
+ end
11
+ end
12
+
13
+ using RunnerExt
14
+
15
+ module StrategyExt
16
+ def association(runner)
17
+ return super unless FactoryDefault.exists?(runner.name)
18
+ FactoryDefault.get(runner.name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,8 +3,8 @@
3
3
  require "test_prof/factory_doctor/factory_girl_patch"
4
4
 
5
5
  module TestProf
6
- # FactoryDoctor is a tool that helps you identify such _bad_ tests,
7
- # i.e. tests that perform unnecessary database queries.
6
+ # FactoryDoctor is a tool that helps you identify
7
+ # tests that perform unnecessary database queries.
8
8
  module FactoryDoctor
9
9
  class Result # :nodoc:
10
10
  attr_reader :count, :time, :queries_count
@@ -81,14 +81,16 @@ module TestProf
81
81
  def within_factory(strategy)
82
82
  return yield if ignore? || !running? || (strategy != :create)
83
83
 
84
- ts = Time.now if @depth.zero?
85
- @depth += 1
86
- @count += 1
87
- yield
88
- ensure
89
- @depth -= 1
84
+ begin
85
+ ts = Time.now if @depth.zero?
86
+ @depth += 1
87
+ @count += 1
88
+ yield
89
+ ensure
90
+ @depth -= 1
90
91
 
91
- @time += (Time.now - ts) if @depth.zero?
92
+ @time += (Time.now - ts) if @depth.zero?
93
+ end
92
94
  end
93
95
 
94
96
  private
@@ -83,9 +83,11 @@ TestProf.activate('FDOC') do
83
83
  RSpec.configure do |config|
84
84
  listener = TestProf::FactoryDoctor::RSpecListener.new
85
85
 
86
- config.reporter.register_listener(
87
- listener, *TestProf::FactoryDoctor::RSpecListener::NOTIFICATIONS
88
- )
86
+ config.before(:suite) do
87
+ config.reporter.register_listener(
88
+ listener, *TestProf::FactoryDoctor::RSpecListener::NOTIFICATIONS
89
+ )
90
+ end
89
91
 
90
92
  config.after(:suite) { listener.print }
91
93
  end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/factory_prof/factory_girl_patch"
4
+ require "test_prof/factory_prof/printers/simple"
5
+ require "test_prof/factory_prof/printers/flamegraph"
6
+
7
+ module TestProf
8
+ # FactoryProf collects "factory stacks" that can be used to build
9
+ # flamegraphs or detect most popular factories
10
+ module FactoryProf
11
+ # FactoryProf configuration
12
+ class Configuration
13
+ attr_accessor :mode
14
+
15
+ def initialize
16
+ @mode = ENV['FPROF'] == 'flamegraph' ? :flamegraph : :simple
17
+ end
18
+
19
+ # Whether we want to generate flamegraphs
20
+ def flamegraph?
21
+ @mode == :flamegraph
22
+ end
23
+ end
24
+
25
+ class Result # :nodoc:
26
+ attr_reader :stacks, :raw_stats
27
+
28
+ def initialize(stacks, raw_stats)
29
+ @stacks = stacks
30
+ @raw_stats = raw_stats
31
+ end
32
+
33
+ # Returns sorted stats
34
+ def stats
35
+ return @stats if instance_variable_defined?(:@stats)
36
+
37
+ @stats = @raw_stats.values
38
+ .sort_by { |el| -el[:total] }
39
+ end
40
+
41
+ def total
42
+ return @total if instance_variable_defined?(:@total)
43
+ @total = @raw_stats.values.sum { |v| v[:total] }
44
+ end
45
+
46
+ private
47
+
48
+ def sorted_stats(key)
49
+ @raw_stats.values
50
+ .map { |el| [el[:name], el[key]] }
51
+ .sort_by { |el| -el[1] }
52
+ end
53
+ end
54
+
55
+ class << self
56
+ include TestProf::Logging
57
+
58
+ def config
59
+ @config ||= Configuration.new
60
+ end
61
+
62
+ def configure
63
+ yield config
64
+ end
65
+
66
+ # Patch factory lib, init vars
67
+ def init
68
+ @running = false
69
+
70
+ log :info, "FactoryProf enabled (#{config.mode} mode)"
71
+
72
+ # Monkey-patch FactoryGirl
73
+ ::FactoryGirl::FactoryRunner.prepend(FactoryGirlPatch) if
74
+ defined?(::FactoryGirl)
75
+ end
76
+
77
+ # Inits FactoryProf and setups at exit hook,
78
+ # then runs
79
+ def run
80
+ init
81
+
82
+ printer = config.flamegraph? ? Printers::Flamegraph : Printers::Simple
83
+
84
+ at_exit { printer.dump(result) }
85
+
86
+ start
87
+ end
88
+
89
+ def start
90
+ reset!
91
+ @running = true
92
+ end
93
+
94
+ def stop
95
+ @running = false
96
+ end
97
+
98
+ def result
99
+ Result.new(@stacks, @stats)
100
+ end
101
+
102
+ def track(strategy, factory)
103
+ return yield if !running? || (strategy != :create)
104
+ begin
105
+ @depth += 1
106
+ @current_stack << factory if config.flamegraph?
107
+ @stats[factory][:total] += 1
108
+ @stats[factory][:top_level] += 1 if @depth == 1
109
+ yield
110
+ ensure
111
+ @depth -= 1
112
+ flush_stack if @depth.zero?
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def reset!
119
+ @stacks = [] if config.flamegraph?
120
+ @depth = 0
121
+ @stats = Hash.new { |h, k| h[k] = { name: k, total: 0, top_level: 0 } }
122
+ flush_stack
123
+ end
124
+
125
+ def flush_stack
126
+ return unless config.flamegraph?
127
+ @stacks << @current_stack unless @current_stack.nil? || @current_stack.empty?
128
+ @current_stack = []
129
+ end
130
+
131
+ def running?
132
+ @running == true
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ TestProf.activate('FPROF') do
139
+ TestProf::FactoryProf.run
140
+ end