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