test-prof 0.1.0.pre5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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