test-prof 0.4.9 → 0.5.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -20
  3. data/README.md +10 -57
  4. data/assets/tagprof.demo.html +447 -0
  5. data/assets/tagprof.template.html +447 -0
  6. data/lib/minitest/event_prof_formatter.rb +18 -16
  7. data/lib/test_prof/any_fixture/dsl.rb +18 -0
  8. data/lib/test_prof/any_fixture.rb +92 -8
  9. data/lib/test_prof/event_prof/custom_events/factory_create.rb +1 -1
  10. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +1 -1
  11. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +1 -1
  12. data/lib/test_prof/event_prof/custom_events.rb +30 -0
  13. data/lib/test_prof/event_prof/minitest.rb +6 -0
  14. data/lib/test_prof/event_prof/profiler.rb +129 -0
  15. data/lib/test_prof/event_prof/rspec.rb +20 -11
  16. data/lib/test_prof/event_prof.rb +10 -108
  17. data/lib/test_prof/ext/active_record_refind.rb +20 -0
  18. data/lib/test_prof/factory_all_stub/factory_bot_patch.rb +13 -0
  19. data/lib/test_prof/factory_all_stub.rb +32 -0
  20. data/lib/test_prof/factory_doctor/rspec.rb +3 -2
  21. data/lib/test_prof/factory_prof/printers/flamegraph.rb +9 -13
  22. data/lib/test_prof/recipes/active_record_one_love.rb +4 -0
  23. data/lib/test_prof/recipes/active_record_shared_connection.rb +55 -0
  24. data/lib/test_prof/recipes/logging.rb +37 -0
  25. data/lib/test_prof/recipes/rspec/any_fixture.rb +4 -1
  26. data/lib/test_prof/recipes/rspec/factory_all_stub.rb +10 -0
  27. data/lib/test_prof/rspec_dissect/rspec.rb +4 -2
  28. data/lib/test_prof/rspec_stamp/rspec.rb +3 -2
  29. data/lib/test_prof/tag_prof/printers/html.rb +24 -0
  30. data/lib/test_prof/tag_prof/printers/simple.rb +82 -0
  31. data/lib/test_prof/tag_prof/result.rb +38 -0
  32. data/lib/test_prof/tag_prof/rspec.rb +43 -40
  33. data/lib/test_prof/tag_prof.rb +4 -0
  34. data/lib/test_prof/utils/html_builder.rb +21 -0
  35. data/lib/test_prof/version.rb +1 -1
  36. data/lib/test_prof.rb +2 -1
  37. metadata +20 -22
  38. data/assets/logo.svg +0 -1
  39. data/assets/testprof.png +0 -0
  40. data/guides/.rubocop.yml +0 -1
  41. data/guides/any_fixture.md +0 -114
  42. data/guides/before_all.md +0 -98
  43. data/guides/event_prof.md +0 -177
  44. data/guides/factory_default.md +0 -111
  45. data/guides/factory_doctor.md +0 -119
  46. data/guides/factory_prof.md +0 -86
  47. data/guides/let_it_be.md +0 -97
  48. data/guides/rspec_dissect.md +0 -60
  49. data/guides/rspec_stamp.md +0 -52
  50. data/guides/rubocop.md +0 -48
  51. data/guides/ruby_prof.md +0 -63
  52. data/guides/stack_prof.md +0 -47
  53. data/guides/tag_prof.md +0 -51
  54. data/guides/tests_sampling.md +0 -24
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/factory_bot"
4
+ require "test_prof/factory_all_stub/factory_bot_patch"
5
+
6
+ module TestProf
7
+ # FactoryAllStub inject into FactoryBot to make
8
+ # all strategies be `build_stubbed` strategy.
9
+ module FactoryAllStub
10
+ LOCAL_NAME = :__factory_bot_stub_all__
11
+
12
+ class << self
13
+ def init
14
+ # Monkey-patch FactoryBot / FactoryGirl
15
+ TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
16
+ defined?(TestProf::FactoryBot)
17
+ end
18
+
19
+ def enabled?
20
+ Thread.current[LOCAL_NAME] == true
21
+ end
22
+
23
+ def enable!
24
+ Thread.current[LOCAL_NAME] = true
25
+ end
26
+
27
+ def disable!
28
+ Thread.current[LOCAL_NAME] = false
29
+ end
30
+ end
31
+ end
32
+ end
@@ -128,15 +128,16 @@ TestProf.activate('FDOC') do
128
128
  TestProf::FactoryDoctor.init
129
129
 
130
130
  RSpec.configure do |config|
131
- listener = TestProf::FactoryDoctor::RSpecListener.new
131
+ listener = nil
132
132
 
133
133
  config.before(:suite) do
134
+ listener = TestProf::FactoryDoctor::RSpecListener.new
134
135
  config.reporter.register_listener(
135
136
  listener, *TestProf::FactoryDoctor::RSpecListener::NOTIFICATIONS
136
137
  )
137
138
  end
138
139
 
139
- config.after(:suite) { listener.print }
140
+ config.after(:suite) { listener.print unless listener.nil? }
140
141
  end
141
142
 
142
143
  RSpec.shared_context "factory_doctor:ignore", fd_ignore: true do
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require "test_prof/utils/html_builder"
4
4
 
5
5
  module TestProf::FactoryProf
6
6
  module Printers
7
7
  module Flamegraph # :nodoc: all
8
+ TEMPLATE = "flamegraph.template.html".freeze
9
+ OUTPUT_NAME = "factory-flame.html".freeze
10
+
8
11
  class << self
9
12
  include TestProf::Logging
10
13
 
@@ -17,7 +20,11 @@ module TestProf::FactoryProf
17
20
 
18
21
  report_data[:roots] = convert_stacks(result)
19
22
 
20
- path = generate_html(report_data)
23
+ path = TestProf::Utils::HTMLBuilder.generate(
24
+ data: report_data,
25
+ template: TEMPLATE,
26
+ output: OUTPUT_NAME
27
+ )
21
28
 
22
29
  log :info, "FactoryFlame report generated: #{path}"
23
30
  end
@@ -55,17 +62,6 @@ module TestProf::FactoryProf
55
62
 
56
63
  res
57
64
  end
58
-
59
- private
60
-
61
- def generate_html(data)
62
- template = File.read(TestProf.asset_path("flamegraph.template.html"))
63
- template.sub! '/**REPORT-DATA**/', data.to_json
64
-
65
- outpath = TestProf.artifact_path("factory-flame.html")
66
- File.write(outpath, template)
67
- outpath
68
- end
69
65
  end
70
66
  end
71
67
  end
@@ -0,0 +1,4 @@
1
+ require_relative "./active_record_shared_connection"
2
+
3
+ # One ❤️
4
+ TestProf::ActiveRecordOneLove = TestProf::ActiveRecordSharedConnection
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Forces ActiveRecord to use the same connection between threads
5
+ module ActiveRecordSharedConnection # :nodoc: all
6
+ class << self
7
+ attr_reader :connection
8
+
9
+ def enable!
10
+ self.connection = ActiveRecord::Base.connection
11
+ end
12
+
13
+ def disable!
14
+ self.connection = nil
15
+ end
16
+
17
+ private
18
+
19
+ def connection=(conn)
20
+ @connection = conn
21
+ connection.singleton_class.prepend Connection
22
+ connection
23
+ end
24
+ end
25
+
26
+ module Connection
27
+ def shared_lock
28
+ @shared_lock ||= Mutex.new
29
+ end
30
+
31
+ def exec_cache(*)
32
+ shared_lock.synchronize { super }
33
+ end
34
+
35
+ def exec_no_cache(*)
36
+ shared_lock.synchronize { super }
37
+ end
38
+
39
+ def execute(*)
40
+ shared_lock.synchronize { super }
41
+ end
42
+ end
43
+
44
+ module Ext
45
+ def connection
46
+ ActiveRecordSharedConnection.connection || super
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ ActiveSupport.on_load(:active_record) do
53
+ TestProf::ActiveRecordSharedConnection.enable!
54
+ ActiveRecord::Base.singleton_class.prepend TestProf::ActiveRecordSharedConnection::Ext
55
+ end
@@ -0,0 +1,37 @@
1
+ if defined?(RSpec)
2
+ RSpec.shared_context "logging:verbose", log: true do
3
+ around(:each) do |ex|
4
+ *loggers = ActiveSupport::LogSubscriber.logger,
5
+ Rails.logger,
6
+ ActiveRecord::Base.logger
7
+ ActiveSupport::LogSubscriber.logger =
8
+ Rails.logger =
9
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
10
+ ex.run
11
+ ActiveSupport::LogSubscriber.logger,
12
+ Rails.logger,
13
+ ActiveRecord::Base.logger = *loggers
14
+ end
15
+ end
16
+
17
+ RSpec.shared_context "logging:active_record", log: :ar do
18
+ around(:each) do |ex|
19
+ logger = ActiveRecord::Base.logger
20
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
21
+ ex.run
22
+ ActiveRecord::Base.logger = logger
23
+ end
24
+ end
25
+ end
26
+
27
+ TestProf.activate("LOG", "all") do
28
+ TestProf.log :info, "Rails verbose logging enabled"
29
+ ActiveSupport::LogSubscriber.logger =
30
+ Rails.logger =
31
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
32
+ end
33
+
34
+ TestProf.activate("LOG", "ar") do
35
+ TestProf.log :info, "Active Record verbose logging enabled"
36
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
37
+ end
@@ -17,5 +17,8 @@ RSpec.shared_context "any_fixture:clean", with_clean_fixture: true do
17
17
  end
18
18
 
19
19
  RSpec.configure do |config|
20
- config.after(:suite) { TestProf::AnyFixture.reset }
20
+ config.after(:suite) do
21
+ TestProf::AnyFixture.report_stats if TestProf::AnyFixture.reporting_enabled?
22
+ TestProf::AnyFixture.reset
23
+ end
21
24
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/factory_all_stub"
4
+
5
+ TestProf::FactoryAllStub.init
6
+
7
+ RSpec.shared_context "factory:stub", factory: :stub do
8
+ prepend_before(:all) { TestProf::FactoryAllStub.enable! }
9
+ append_after(:all) { TestProf::FactoryAllStub.disable! }
10
+ end
@@ -157,14 +157,16 @@ end
157
157
  # Register RSpecDissect listener
158
158
  TestProf.activate('RD_PROF') do
159
159
  RSpec.configure do |config|
160
- listener = TestProf::RSpecDissect::Listener.new
160
+ listener = nil
161
161
 
162
162
  config.before(:suite) do
163
+ listener = TestProf::RSpecDissect::Listener.new
164
+
163
165
  config.reporter.register_listener(
164
166
  listener, *TestProf::RSpecDissect::Listener::NOTIFICATIONS
165
167
  )
166
168
  end
167
169
 
168
- config.after(:suite) { listener.print }
170
+ config.after(:suite) { listener.print unless listener.nil? }
169
171
  end
170
172
  end
@@ -58,14 +58,15 @@ end
58
58
  # Register EventProf listener
59
59
  TestProf.activate('RSTAMP') do
60
60
  RSpec.configure do |config|
61
- listener = TestProf::RSpecStamp::RSpecListener.new
61
+ listener = nil
62
62
 
63
63
  config.before(:suite) do
64
+ listener = TestProf::RSpecStamp::RSpecListener.new
64
65
  config.reporter.register_listener(
65
66
  listener, *TestProf::RSpecStamp::RSpecListener::NOTIFICATIONS
66
67
  )
67
68
  end
68
69
 
69
- config.after(:suite) { listener.stamp! }
70
+ config.after(:suite) { listener.stamp! unless listener.nil? }
70
71
  end
71
72
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf::TagProf
4
+ module Printers
5
+ module HTML # :nodoc: all
6
+ TEMPLATE = "tagprof.template.html".freeze
7
+ OUTPUT_NAME = "tag-prof.html".freeze
8
+
9
+ class << self
10
+ include TestProf::Logging
11
+
12
+ def dump(result)
13
+ path = TestProf::Utils::HTMLBuilder.generate(
14
+ data: result,
15
+ template: TEMPLATE,
16
+ output: OUTPUT_NAME
17
+ )
18
+
19
+ log :info, "TagProf report generated: #{path}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/ext/float_duration"
4
+ require "test_prof/ext/string_strip_heredoc"
5
+
6
+ module TestProf::TagProf
7
+ module Printers
8
+ module Simple # :nodoc: all
9
+ class << self
10
+ include TestProf::Logging
11
+ using TestProf::StringStripHeredoc
12
+ using TestProf::FloatDuration
13
+
14
+ def dump(result)
15
+ msgs = []
16
+
17
+ msgs <<
18
+ <<-MSG.strip_heredoc
19
+ TagProf report for #{result.tag}
20
+ MSG
21
+
22
+ header = []
23
+
24
+ header << format(
25
+ '%15s %12s ',
26
+ result.tag, 'time'
27
+ )
28
+
29
+ events_format = nil
30
+
31
+ unless result.events.empty?
32
+ events_format = result.events.map { |event| "%#{event.size + 2}s " }.join
33
+
34
+ header << format(
35
+ events_format,
36
+ *result.events
37
+ )
38
+ end
39
+
40
+ header << format(
41
+ '%6s %6s %6s %12s',
42
+ 'total', '%total', '%time', 'avg'
43
+ )
44
+
45
+ msgs << header.join
46
+
47
+ msgs << ""
48
+
49
+ total = result.data.values.inject(0) { |acc, v| acc + v[:count] }
50
+ total_time = result.data.values.inject(0) { |acc, v| acc + v[:time] }
51
+
52
+ result.data.values.sort_by { |v| -v[:time] }.each do |tag|
53
+ line = []
54
+ line << format(
55
+ "%15s %12s ",
56
+ tag[:value], tag[:time].duration
57
+ )
58
+
59
+ unless result.events.empty?
60
+ line << format(
61
+ events_format,
62
+ *result.events.map { |event| tag[event].duration }
63
+ )
64
+ end
65
+
66
+ line << format(
67
+ "%6d %6.2f %6.2f %12s",
68
+ tag[:count],
69
+ 100 * tag[:count].to_f / total,
70
+ 100 * tag[:time] / total_time,
71
+ (tag[:time] / tag[:count]).duration
72
+ )
73
+
74
+ msgs << line.join
75
+ end
76
+
77
+ log :info, msgs.join("\n")
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module TagProf # :nodoc:
5
+ # Object holding all the stats for tags
6
+ class Result
7
+ attr_reader :tag, :data, :events
8
+
9
+ def initialize(tag, events = [])
10
+ @tag = tag
11
+ @events = events
12
+
13
+ @data = Hash.new do |h, k|
14
+ h[k] = { value: k, count: 0, time: 0.0 }
15
+ h[k].merge!(Hash[events.map { |event| [event, 0.0] }]) unless
16
+ events.empty?
17
+ h[k]
18
+ end
19
+ end
20
+
21
+ def track(tag, time:, events: {})
22
+ data[tag][:count] += 1
23
+ data[tag][:time] += time
24
+ events.each do |k, v|
25
+ data[tag][k] += v
26
+ end
27
+ end
28
+
29
+ def to_json
30
+ {
31
+ tag: tag,
32
+ data: data.values,
33
+ events: events
34
+ }.to_json
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof/ext/float_duration"
4
- require "test_prof/ext/string_strip_heredoc"
5
-
6
3
  module TestProf
7
4
  module TagProf
8
5
  class RSpecListener # :nodoc:
9
6
  include Logging
10
- using FloatDuration
11
- using StringStripHeredoc
12
7
 
13
8
  NOTIFICATIONS = %i[
14
9
  example_started
@@ -16,58 +11,58 @@ module TestProf
16
11
  example_passed
17
12
  ].freeze
18
13
 
14
+ attr_reader :result, :printer
15
+
19
16
  def initialize
20
- @tag = ENV['TAG_PROF'].to_sym
21
- @tags = Hash.new { |h, k| h[k] = { val: k, count: 0, time: 0.0 } }
17
+ @printer = ENV['TAG_PROF_FORMAT'] == 'html' ? Printers::HTML : Printers::Simple
18
+
19
+ @result =
20
+ if ENV['TAG_PROF_EVENT'].nil?
21
+ Result.new ENV['TAG_PROF'].to_sym
22
+ else
23
+ require "test_prof/event_prof"
24
+
25
+ @events_profiler = EventProf.build(ENV['TAG_PROF_EVENT'])
22
26
 
23
- log :info, "TagProf enabled (#{@tag})"
27
+ Result.new ENV['TAG_PROF'].to_sym, @events_profiler.events
28
+ end
29
+
30
+ log :info, "TagProf enabled (#{result.tag})"
24
31
  end
25
32
 
26
33
  def example_started(_notification)
27
34
  @ts = TestProf.now
35
+ # enable event profiling
36
+ @events_profiler.group_started(true) if @events_profiler
28
37
  end
29
38
 
30
39
  def example_finished(notification)
31
- tag = notification.example.metadata.fetch(@tag, :__unknown__)
40
+ tag = notification.example.metadata.fetch(result.tag, :__unknown__)
41
+
42
+ result.track(tag, time: TestProf.now - @ts, events: fetch_events_data)
32
43
 
33
- @tags[tag][:count] += 1
34
- @tags[tag][:time] += (TestProf.now - @ts)
44
+ # reset and disable event profilers
45
+ @events_profiler.group_started(nil) if @events_profiler
35
46
  end
36
47
 
37
48
  # NOTE: RSpec < 3.4.0 doesn't have example_finished event
38
49
  alias example_passed example_finished
39
50
  alias example_failed example_finished
40
51
 
41
- def print
42
- msgs = []
43
-
44
- msgs <<
45
- <<-MSG.strip_heredoc
46
- TagProf report for #{@tag}
47
- MSG
48
-
49
- msgs << format(
50
- "%15s %12s %6s %6s %6s %12s",
51
- @tag,
52
- 'time', 'total', '%total', '%time', 'avg'
53
- )
54
-
55
- msgs << ""
52
+ def report
53
+ printer.dump(result)
54
+ end
56
55
 
57
- total = @tags.values.inject(0) { |acc, v| acc + v[:count] }
58
- total_time = @tags.values.inject(0) { |acc, v| acc + v[:time] }
56
+ private
59
57
 
60
- @tags.values.sort_by { |v| -v[:time] }.each do |tag|
61
- msgs << format(
62
- "%15s %12s %6d %6.2f %6.2f %12s",
63
- tag[:val], tag[:time].duration, tag[:count],
64
- 100 * tag[:count].to_f / total,
65
- 100 * tag[:time] / total_time,
66
- (tag[:time] / tag[:count]).duration
67
- )
68
- end
58
+ def fetch_events_data
59
+ return {} unless @events_profiler
69
60
 
70
- log :info, msgs.join("\n")
61
+ Hash[
62
+ @events_profiler.profilers.map do |profiler|
63
+ [profiler.event, profiler.time]
64
+ end
65
+ ]
71
66
  end
72
67
  end
73
68
  end
@@ -76,14 +71,22 @@ end
76
71
  # Register TagProf listener
77
72
  TestProf.activate('TAG_PROF') do
78
73
  RSpec.configure do |config|
79
- listener = TestProf::TagProf::RSpecListener.new
74
+ listener = nil
80
75
 
81
76
  config.before(:suite) do
77
+ listener = TestProf::TagProf::RSpecListener.new
82
78
  config.reporter.register_listener(
83
79
  listener, *TestProf::TagProf::RSpecListener::NOTIFICATIONS
84
80
  )
85
81
  end
86
82
 
87
- config.after(:suite) { listener.print }
83
+ config.after(:suite) { listener.report unless listener.nil? }
88
84
  end
89
85
  end
86
+
87
+ # Activate custom events
88
+ TestProf.activate('TAG_PROF_EVENT') do
89
+ require "test_prof/event_prof"
90
+
91
+ TestProf::EventProf::CustomEvents.activate_all(ENV['TAG_PROF_EVENT'])
92
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/tag_prof/result"
4
+ require "test_prof/tag_prof/printers/simple"
5
+ require "test_prof/tag_prof/printers/html"
6
+
3
7
  module TestProf
4
8
  module TagProf # :nodoc:
5
9
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TestProf
6
+ module Utils
7
+ # Generates static HTML reports with injected data
8
+ module HTMLBuilder
9
+ class << self
10
+ def generate(data:, template:, output:)
11
+ template = File.read(TestProf.asset_path(template))
12
+ template.sub! '/**REPORT-DATA**/', data.to_json
13
+
14
+ outpath = TestProf.artifact_path(output)
15
+ File.write(outpath, template)
16
+ outpath
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "0.4.9".freeze
4
+ VERSION = "0.5.0".freeze
5
5
  end
data/lib/test_prof.rb CHANGED
@@ -90,7 +90,7 @@ module TestProf
90
90
  private
91
91
 
92
92
  def activate!(env_var, val)
93
- yield if ENV[env_var] && (val.nil? || ENV[env_var] == val)
93
+ yield if ENV[env_var] && (val.nil? || val === ENV[env_var])
94
94
  end
95
95
 
96
96
  def with_timestamps(path)
@@ -132,3 +132,4 @@ require "test_prof/factory_prof"
132
132
  require "test_prof/rspec_stamp"
133
133
  require "test_prof/tag_prof"
134
134
  require "test_prof/rspec_dissect"
135
+ require "test_prof/factory_all_stub"