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
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/factory_doctor/factory_girl_patch"
4
+
5
+ module TestProf
6
+ # FactoryDoctor is a tool that helps you identify
7
+ # tests that perform unnecessary database queries.
8
+ module FactoryDoctor
9
+ class Result # :nodoc:
10
+ attr_reader :count, :time, :queries_count
11
+
12
+ def initialize(count, time, queries_count)
13
+ @count = count
14
+ @time = time
15
+ @queries_count = queries_count
16
+ end
17
+
18
+ def bad?
19
+ count.positive? && queries_count.zero?
20
+ end
21
+ end
22
+
23
+ IGNORED_QUERIES_PATTERN = %r{(
24
+ pg_table|
25
+ pg_attribute|
26
+ pg_namespace|
27
+ show\stables|
28
+ pragma|
29
+ sqlite_master/rollback|
30
+ \ATRUNCATE TABLE|
31
+ \AALTER TABLE|
32
+ \ABEGIN|
33
+ \ACOMMIT|
34
+ \AROLLBACK|
35
+ \ARELEASE|
36
+ \ASAVEPOINT
37
+ )}xi
38
+
39
+ class << self
40
+ include TestProf::Logging
41
+
42
+ attr_reader :event
43
+ attr_reader :count, :time, :queries_count
44
+
45
+ # Patch factory lib, init counters
46
+ def init(event = 'sql.active_record')
47
+ @event = event
48
+ reset!
49
+
50
+ log :info, "FactoryDoctor enabled"
51
+
52
+ # Monkey-patch FactoryGirl
53
+ ::FactoryGirl::FactoryRunner.prepend(FactoryGirlPatch) if
54
+ defined?(::FactoryGirl)
55
+
56
+ subscribe!
57
+ end
58
+
59
+ def start
60
+ reset!
61
+ @running = true
62
+ end
63
+
64
+ def stop
65
+ @running = false
66
+ end
67
+
68
+ def result
69
+ Result.new(count, time, queries_count)
70
+ end
71
+
72
+ # Do not analyze code within the block
73
+ def ignore
74
+ @ignored = true
75
+ res = yield
76
+ ensure
77
+ @ignored = false
78
+ res
79
+ end
80
+
81
+ def within_factory(strategy)
82
+ return yield if ignore? || !running? || (strategy != :create)
83
+
84
+ begin
85
+ ts = Time.now if @depth.zero?
86
+ @depth += 1
87
+ @count += 1
88
+ yield
89
+ ensure
90
+ @depth -= 1
91
+
92
+ @time += (Time.now - ts) if @depth.zero?
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def reset!
99
+ @depth = 0
100
+ @time = 0.0
101
+ @count = 0
102
+ @queries_count = 0
103
+ end
104
+
105
+ def subscribe!
106
+ ::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
107
+ next if ignore? || !running? || within_factory?
108
+ next if query[:sql] =~ IGNORED_QUERIES_PATTERN
109
+ @queries_count += 1
110
+ end
111
+ end
112
+
113
+ def within_factory?
114
+ @depth.positive?
115
+ end
116
+
117
+ def ignore?
118
+ @ignored == true
119
+ end
120
+
121
+ def running?
122
+ @running == true
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ require "test_prof/factory_doctor/rspec" if defined?(RSpec)
129
+ require "test_prof/factory_doctor/minitest" if defined?(Minitest::Reporters)
130
+
131
+ TestProf.activate('FDOC') do
132
+ TestProf::FactoryDoctor.init
133
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module FactoryProf
5
+ # Wrap #run method with FactoryProf tracking
6
+ module FactoryGirlPatch
7
+ def run(strategy = @strategy)
8
+ FactoryProf.track(strategy, @name) { super }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TestProf::FactoryProf
6
+ module Printers
7
+ module Flamegraph # :nodoc: all
8
+ class << self
9
+ include TestProf::Logging
10
+
11
+ def dump(result)
12
+ report_data = {
13
+ total_stacks: result.stacks.size,
14
+ total: result.total
15
+ }
16
+
17
+ report_data[:roots] = convert_stacks(result)
18
+
19
+ path = generate_html(report_data)
20
+
21
+ log :info, "FactoryFlame report generated: #{path}"
22
+ end
23
+
24
+ def convert_stacks(result)
25
+ res = []
26
+
27
+ paths = {}
28
+
29
+ result.stacks.each do |stack|
30
+ parent = nil
31
+ path = ""
32
+
33
+ stack.each do |sample|
34
+ path = "#{path}/#{sample}"
35
+
36
+ if paths[path]
37
+ node = paths[path]
38
+ node[:value] += 1
39
+ else
40
+ node = { name: sample, value: 1, total: result.raw_stats.fetch(sample)[:total] }
41
+ paths[path] = node
42
+
43
+ if parent.nil?
44
+ res << node
45
+ else
46
+ parent[:children] ||= []
47
+ parent[:children] << node
48
+ end
49
+ end
50
+
51
+ parent = node
52
+ end
53
+ end
54
+
55
+ res
56
+ end
57
+
58
+ private
59
+
60
+ def generate_html(data)
61
+ template = File.read(TestProf.asset_path("flamegraph.template.html"))
62
+ template.sub! '/**REPORT-DATA**/', data.to_json
63
+
64
+ outpath = TestProf.artefact_path("factory-flame.html")
65
+ File.write(outpath, template)
66
+ outpath
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf::FactoryProf
4
+ module Printers
5
+ module Simple # :nodoc: all
6
+ class << self
7
+ include TestProf::Logging
8
+
9
+ def dump(result)
10
+ msgs = []
11
+
12
+ msgs <<
13
+ <<~MSG
14
+ Factories usage
15
+
16
+ total top-level name
17
+ MSG
18
+
19
+ result.stats.each do |stat|
20
+ msgs << format("%6d %11d %15s", stat[:total], stat[:top_level], stat[:name])
21
+ end
22
+
23
+ log :info, msgs.join("\n")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ 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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Helper for output printing
5
+ module Logging
6
+ COLORS = {
7
+ info: "\e[34m", # blue
8
+ error: "\e[31m", # red
9
+ }.freeze
10
+
11
+ def log(level, msg)
12
+ TestProf.config.output.puts(build_log_msg(level, msg))
13
+ end
14
+
15
+ def build_log_msg(level, msg)
16
+ colorize(level, "[TEST PROF #{level.to_s.upcase}] #{msg}")
17
+ end
18
+
19
+ def colorize(level, msg)
20
+ return msg unless TestProf.config.color?
21
+
22
+ "#{COLORS[level]}#{msg}\e[0m"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/any_fixture"
4
+
5
+ RSpec.shared_context "any_fixture:clean", with_clean_fixture: true do
6
+ before do
7
+ raise("Cannot use clean context without a transaction!") unless
8
+ open_transaction?
9
+
10
+ TestProf::AnyFixture.clean
11
+ end
12
+
13
+ def open_transaction?
14
+ pool = ActiveRecord::Base.connection_pool
15
+ pool.active_connection? && pool.connection.open_transactions.positive?
16
+ end
17
+ end
18
+
19
+ RSpec.configure do |config|
20
+ config.after(:suite) { TestProf::AnyFixture.reset }
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Helper to wrap the whole example group into a transaction
5
+ module BeforeAll
6
+ def before_all(&block)
7
+ raise ArgumentError, "Block is required!" unless block_given?
8
+
9
+ before(:all) do
10
+ ActiveRecord::Base.connection.begin_transaction(joinable: false)
11
+ instance_eval(&block)
12
+ end
13
+
14
+ after(:all) do
15
+ ActiveRecord::Base.connection.rollback_transaction
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ RSpec.configure do |config|
22
+ config.extend TestProf::BeforeAll
23
+ end