test-prof 1.4.4 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cfa0f1751f398092de8ffa46d5bff81bb9eadd7b90caaf9293c62c93c6b3e20
4
- data.tar.gz: 3afcb831782f0e58881839f33af07a41b3b42d63ac8eb87c35b6fd0eb01537ea
3
+ metadata.gz: 119118a5840991b0d1bde28e49aca9f3f72f77f89ba57704c53ce2e8a023ad06
4
+ data.tar.gz: 6f124770cfeba6346f744dbf66a00b118c6c207711af9f18977abea153de4f7a
5
5
  SHA512:
6
- metadata.gz: 2de8fb8232f5ed623167846c37cce6224b297761324acdf6a17a47338e2a3904aa480b48fc9793a51f5595d3d839c3ff3bc9fbb262c7900ad3d7c819113947a0
7
- data.tar.gz: dcec92d722f9e7cce9f1d371a2c6d363aa9a78a6cf1149a73d8b836a269ebf6bd14e50d86a8470cccc56a861b94646ad23494f43de6d9beb0cc6a5b478f77e93
6
+ metadata.gz: 6827f8b914ce78da9cabff0dd04ef643c3571e22864a1c6c215028fd4a2e9f79dbbab0d69e7ac9184753460aee96a2202a9201f7bdf2177074b42de47e893ac4
7
+ data.tar.gz: 4beacf5a982c0fe64c1c363660f2985be9dd6470ac847eb10e5ef4cd3ddf3360681e8c09fb511d5a99418629b455755e93824768c21520d0c754a908a7e7080c
data/CHANGELOG.md CHANGED
@@ -2,9 +2,33 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 1.5.0 (2025-12-04)
6
+
7
+ - Logging: support Rails 8.2 structured events based logging. ([@palkan][])
8
+
9
+ - Allow using AnyFixture DSL through module inclusion, not refinements. ([@palkan][])
10
+
11
+ In Rails 7.2+, refined `#fixture` no longer works since there is a same-called method. So, from now on we recommend including the DSL module, instead of _using_ it.
12
+
13
+ ## 1.4.5. (2025-05-09) 🎇
14
+
15
+ - FactoryProf: Add truncate_names configuration parameter. ([@skaestle][])
16
+
17
+ - Update Rubocop setup to support new plugins system. ([@julianpasquale])
18
+
19
+ Now you can truncate long factory-names when using the simple output mode.
20
+
21
+ Set `FPROF_TRUNCATE_NAMES=1` env var or set it through `FactoryProf` configuration:
22
+
23
+ ```ruby
24
+ TestProf::FactoryProf.configure do |config|
25
+ config.truncate_names = true
26
+ end
27
+ ```
28
+
5
29
  ## 1.4.4 (2025-01-03)
6
30
 
7
- - Fix _stamping_ specs with single quotes with Rpsec Stamp. ([@elasticspoon][])
31
+ - Fix _stamping_ specs with single quotes with RSpec Stamp. ([@elasticspoon][])
8
32
 
9
33
  ## 1.4.3 (2024-12-18)
10
34
 
@@ -9,10 +9,7 @@ module TestProf
9
9
  module AnyFixture
10
10
  # Adds "global" `fixture`, `before_fixtures_reset` and `after_fixtures_reset` methods (through refinement)
11
11
  module DSL
12
- # Refine object, 'cause refining modules (Kernel) is vulnerable to prepend:
13
- # - https://bugs.ruby-lang.org/issues/13446
14
- # - Rails added `Kernel.prepend` in 6.1: https://github.com/rails/rails/commit/3124007bd674dcdc9c3b5c6b2964dfb7a1a0733c
15
- refine ::Object do
12
+ module Methods
16
13
  def fixture(id, &block)
17
14
  id = :"#{id}"
18
15
  record = ::TestProf::AnyFixture.cached(id)
@@ -38,6 +35,21 @@ module TestProf
38
35
  ::TestProf::AnyFixture.after_fixtures_reset(&block)
39
36
  end
40
37
  end
38
+
39
+ def self.included(base)
40
+ base.include Methods
41
+ end
42
+
43
+ # Refine object, 'cause refining modules (Kernel) is vulnerable to prepend:
44
+ # - https://bugs.ruby-lang.org/issues/13446
45
+ # - Rails added `Kernel.prepend` in 6.1: https://github.com/rails/rails/commit/3124007bd674dcdc9c3b5c6b2964dfb7a1a0733c
46
+ refine ::Object do
47
+ if RUBY_VERSION >= "3.1.0"
48
+ import_methods Methods
49
+ else
50
+ include Methods
51
+ end
52
+ end
41
53
  end
42
54
  end
43
55
  end
@@ -24,6 +24,11 @@ module TestProf
24
24
  end
25
25
 
26
26
  def subscribe!
27
+ Thread.current[:before_all_subscription_count] ||= 0
28
+ Thread.current[:before_all_subscription_count] += 1
29
+
30
+ return unless Thread.current[:before_all_subscription_count] == 1
31
+
27
32
  Thread.current[:before_all_connection_subscriber] = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
28
33
  connection_name = payload[:connection_name] if payload.key?(:connection_name)
29
34
  shard = payload[:shard] if payload.key?(:shard)
@@ -37,7 +42,12 @@ module TestProf
37
42
  end
38
43
 
39
44
  def unsubscribe!
40
- return unless Thread.current[:before_all_connection_subscriber]
45
+ return unless Thread.current[:before_all_subscription_count]
46
+
47
+ Thread.current[:before_all_subscription_count] -= 1
48
+
49
+ return unless Thread.current[:before_all_subscription_count] == 0 && Thread.current[:before_all_connection_subscriber]
50
+
41
51
  ActiveSupport::Notifications.unsubscribe(Thread.current[:before_all_connection_subscriber])
42
52
  Thread.current[:before_all_connection_subscriber] = nil
43
53
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lint_roller"
4
+
5
+ module RuboCop
6
+ module TestProf
7
+ # A plugin that integrates TestProf with RuboCop's plugin system.
8
+ class Plugin < LintRoller::Plugin
9
+ def about
10
+ LintRoller::About.new(
11
+ name: "test_prof",
12
+ version: VERSION,
13
+ homepage: "https://test-prof.evilmartians.io/misc/rubocop",
14
+ description: "RuboCop plugin to help you write more performant tests."
15
+ )
16
+ end
17
+
18
+ def supported?(context)
19
+ context.engine == :rubocop
20
+ end
21
+
22
+ def rules(_context)
23
+ LintRoller::Rules.new(
24
+ type: :path,
25
+ config_format: :rubocop,
26
+ value: Pathname.new(__dir__).join("../../../config/default.yml")
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -29,7 +29,7 @@ TestProf::EventProf::CustomEvents.register("factory.create") do
29
29
  TestProf.log(
30
30
  :error,
31
31
  <<~MSG
32
- Failed to load factory_bot / factory_girl / fabrication.
32
+ Failed to load factory_bot / fabrication.
33
33
 
34
34
  Make sure that any of them is in your Gemfile.
35
35
  MSG
@@ -7,6 +7,7 @@ module TestProf
7
7
  module EventProf
8
8
  class RSpecListener # :nodoc:
9
9
  include Logging
10
+
10
11
  using FloatDuration
11
12
  using StringTruncate
12
13
 
@@ -12,7 +12,7 @@ module TestProf
12
12
 
13
13
  class << self
14
14
  def init
15
- # Monkey-patch FactoryBot / FactoryGirl
15
+ # Monkey-patch FactoryBot
16
16
  TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
17
17
  defined?(TestProf::FactoryBot)
18
18
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf # :nodoc: all
4
- FACTORY_GIRL_NAMES = {"factory_bot" => "::FactoryBot", "factory_girl" => "::FactoryGirl"}.freeze
5
-
6
4
  TestProf.require("active_support")
7
5
 
8
- FACTORY_GIRL_NAMES.find do |name, cname|
9
- TestProf.require(name) do
10
- TestProf::FactoryBot = Object.const_get(cname)
11
- end
6
+ TestProf.require("factory_bot") do
7
+ TestProf::FactoryBot = Object.const_get("::FactoryBot")
12
8
  end
13
9
  end
@@ -6,6 +6,7 @@ module TestProf
6
6
  module FactoryDoctor
7
7
  class RSpecListener # :nodoc:
8
8
  include Logging
9
+
9
10
  using FloatDuration
10
11
 
11
12
  SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'
@@ -27,6 +27,7 @@ module TestProf
27
27
  pg_attribute|
28
28
  pg_namespace|
29
29
  show\stables|
30
+ show\ssearch_path|
30
31
  pragma|
31
32
  sqlite_master/rollback|
32
33
  \ATRUNCATE TABLE|
@@ -70,7 +71,7 @@ module TestProf
70
71
 
71
72
  log :info, "FactoryDoctor enabled (event: \"#{config.event}\", threshold: #{config.threshold})"
72
73
 
73
- # Monkey-patch FactoryBot / FactoryGirl
74
+ # Monkey-patch FactoryBot
74
75
  TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
75
76
  defined?(TestProf::FactoryBot)
76
77
 
@@ -12,7 +12,7 @@ module TestProf
12
12
  class FactoryBot
13
13
  using TestProf::FactoryBotStrategy
14
14
 
15
- # Monkey-patch FactoryBot / FactoryGirl
15
+ # Monkey-patch FactoryBot
16
16
  def self.patch
17
17
  TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
18
18
  defined? TestProf::FactoryBot
@@ -9,7 +9,7 @@ module TestProf::FactoryProf
9
9
  using TestProf::FloatDuration
10
10
  include TestProf::Logging
11
11
 
12
- def dump(result, start_time:, threshold:)
12
+ def dump(result, start_time:, threshold:, truncate_names:)
13
13
  return log(:info, "No factories detected") if result.raw_stats == {}
14
14
  msgs = []
15
15
 
@@ -19,6 +19,12 @@ module TestProf::FactoryProf
19
19
  total_time = result.stats.sum { |stat| stat[:top_level_time] }
20
20
  total_uniq_factories = result.stats.map { |stat| stat[:name] }.uniq.count
21
21
 
22
+ table_indent = 3
23
+ variations_indent = 2
24
+ max_name_length = result.stats.map { _1[:name].length }.max
25
+ max_variation_length = result.stats.flat_map { _1[:variations] }.select(&:present?).map { _1[:name].length }.max || 0
26
+ name_column_length = truncate_names ? 20 : ([max_name_length, max_variation_length].max + variations_indent)
27
+
22
28
  msgs <<
23
29
  <<~MSG
24
30
  Factories usage
@@ -27,14 +33,24 @@ module TestProf::FactoryProf
27
33
  Total top-level: #{total_top_level_count}
28
34
  Total time: #{total_time.duration} (out of #{total_run_time.duration})
29
35
  Total uniq factories: #{total_uniq_factories}
30
-
31
- name total top-level total time time per call top-level time
32
36
  MSG
33
37
 
38
+ msgs << format(
39
+ "%#{table_indent}s%-#{name_column_length}s %8s %12s %13s %16s %17s",
40
+ "", "name", "total", "top-level", "total time", "time per call", "top-level time"
41
+ )
42
+ msgs << ""
43
+
34
44
  result.stats.each do |stat|
35
45
  next if stat[:total_count] < threshold
36
46
 
37
- msgs << format("%-3s%-20s %8d %11d %13.4fs %17.4fs %18.4fs", *format_args(stat))
47
+ msgs << formatted(
48
+ table_indent,
49
+ name_column_length,
50
+ truncate_names,
51
+ stat
52
+ )
53
+
38
54
  # move other variation ("[...]") to the end of the array
39
55
  sorted_variations = stat[:variations].sort_by.with_index do |variation, i|
40
56
  (variation[:name] == "[...]") ? stat[:variations].size + 1 : i
@@ -42,7 +58,12 @@ module TestProf::FactoryProf
42
58
  sorted_variations.each do |variation_stat|
43
59
  next if variation_stat[:total_count] < threshold
44
60
 
45
- msgs << format("%-5s%-18s %8d %11d %13.4fs %17.4fs %18.4fs", *format_args(variation_stat))
61
+ msgs << formatted(
62
+ table_indent + variations_indent,
63
+ name_column_length - variations_indent,
64
+ truncate_names,
65
+ variation_stat
66
+ )
46
67
  end
47
68
  end
48
69
 
@@ -51,6 +72,10 @@ module TestProf::FactoryProf
51
72
 
52
73
  private
53
74
 
75
+ def formatted(indent_len, name_len, truncate_names, stat)
76
+ format(format_string(indent_len, name_len, truncate_names), *format_args(stat))
77
+ end
78
+
54
79
  def format_args(stat)
55
80
  time_per_call = stat[:total_time] / stat[:total_count]
56
81
  format_args = [""]
@@ -58,6 +83,11 @@ module TestProf::FactoryProf
58
83
  format_args << time_per_call
59
84
  format_args << stat[:top_level_time]
60
85
  end
86
+
87
+ def format_string(indent_len, name_len, truncate_names)
88
+ name_format = truncate_names ? "#{name_len}.#{name_len}" : name_len.to_s
89
+ "%#{indent_len}s%-#{name_format}s %8d %12d %12.4fs %15.4fs %16.4fs"
90
+ end
61
91
  end
62
92
  end
63
93
  end
@@ -16,7 +16,8 @@ module TestProf
16
16
 
17
17
  # FactoryProf configuration
18
18
  class Configuration
19
- attr_accessor :mode, :printer, :threshold, :include_variations, :variations_limit
19
+ attr_accessor :mode, :printer, :threshold, :include_variations, :variations_limit,
20
+ :truncate_names
20
21
 
21
22
  def initialize
22
23
  @mode = (ENV["FPROF"] == "flamegraph") ? :flamegraph : :simple
@@ -34,6 +35,7 @@ module TestProf
34
35
  @threshold = ENV.fetch("FPROF_THRESHOLD", 0).to_i
35
36
  @include_variations = ENV["FPROF_VARS"] == "1"
36
37
  @variations_limit = ENV.fetch("FPROF_VARIATIONS_LIMIT", 2).to_i
38
+ @truncate_names = ENV["FPROF_TRUNCATE_NAMES"] == "1"
37
39
  end
38
40
 
39
41
  # Whether we want to generate flamegraphs
@@ -120,7 +122,7 @@ module TestProf
120
122
  def print(started_at)
121
123
  printer = config.printer
122
124
 
123
- printer.dump(result, start_time: started_at, threshold: config.threshold)
125
+ printer.dump(result, start_time: started_at, threshold: config.threshold, truncate_names: config.truncate_names)
124
126
  end
125
127
 
126
128
  def start
@@ -158,7 +160,7 @@ module TestProf
158
160
 
159
161
  def variation_name(variation)
160
162
  return "-" if variation.empty?
161
- variations_count = variation.to_s.scan(/[\w]+/).size
163
+ variations_count = variation.to_s.scan(/\w+/).size
162
164
  return "[...]" if variations_count > config.variations_limit
163
165
 
164
166
  variation
@@ -7,6 +7,7 @@ module TestProf
7
7
  module MemoryProf
8
8
  class Printer
9
9
  include Logging
10
+
10
11
  using StringTruncate
11
12
 
12
13
  def initialize(tracker)
@@ -84,16 +84,30 @@ module TestProf
84
84
 
85
85
  # Enable verbose Rails logging within a block
86
86
  def with_logging
87
+ if ::ActiveSupport.respond_to?(:event_reporter)
88
+ @was_events_debug_mode = ActiveSupport.event_reporter.debug_mode?
89
+ ::ActiveSupport.event_reporter.debug_mode = true
90
+ end
87
91
  *loggers = LoggingHelpers.swap_logger(LoggingHelpers.all_loggables)
88
92
  yield
89
93
  ensure
94
+ if ::ActiveSupport.respond_to?(:event_reporter)
95
+ ::ActiveSupport.event_reporter.debug_mode = @was_events_debug_mode
96
+ end
90
97
  LoggingHelpers.restore_logger(loggers, LoggingHelpers.all_loggables)
91
98
  end
92
99
 
93
100
  def with_ar_logging
101
+ if ::ActiveSupport.respond_to?(:event_reporter)
102
+ @was_events_debug_mode = ActiveSupport.event_reporter.debug_mode?
103
+ ::ActiveSupport.event_reporter.debug_mode = true
104
+ end
94
105
  *loggers = LoggingHelpers.swap_logger(LoggingHelpers.ar_loggables)
95
106
  yield
96
107
  ensure
108
+ if ::ActiveSupport.respond_to?(:event_reporter)
109
+ ::ActiveSupport.event_reporter.debug_mode = @was_events_debug_mode
110
+ end
97
111
  LoggingHelpers.restore_logger(loggers, LoggingHelpers.ar_loggables)
98
112
  end
99
113
  end
@@ -124,10 +138,16 @@ end
124
138
 
125
139
  TestProf.activate("LOG", "all") do
126
140
  TestProf.log :info, "Rails verbose logging enabled"
141
+ if ::ActiveSupport.respond_to?(:event_reporter)
142
+ ::ActiveSupport.event_reporter.debug_mode = true
143
+ end
127
144
  TestProf::Rails::LoggingHelpers.swap_logger!(TestProf::Rails::LoggingHelpers.all_loggables)
128
145
  end
129
146
 
130
147
  TestProf.activate("LOG", "ar") do
131
148
  TestProf.log :info, "Active Record verbose logging enabled"
149
+ if ::ActiveSupport.respond_to?(:event_reporter)
150
+ ::ActiveSupport.event_reporter.debug_mode = true
151
+ end
132
152
  TestProf::Rails::LoggingHelpers.swap_logger!(TestProf::Rails::LoggingHelpers.ar_loggables)
133
153
  end
@@ -6,6 +6,7 @@ module TestProf
6
6
  module RSpecDissect
7
7
  class Listener # :nodoc:
8
8
  include Logging
9
+
9
10
  using FloatDuration
10
11
 
11
12
  NOTIFICATIONS = %i[
@@ -9,5 +9,5 @@ end
9
9
 
10
10
  require "rubocop"
11
11
 
12
- require "test_prof/cops/inject"
12
+ require "test_prof/cops/plugin"
13
13
  require "test_prof/cops/rspec/aggregate_examples"
@@ -72,7 +72,7 @@ module TestProf
72
72
 
73
73
  @locked = true
74
74
 
75
- log :info, "StackProf#{config.raw? ? " (raw)" : ""} enabled globally: " \
75
+ log :info, "StackProf#{" (raw)" if config.raw?} enabled globally: " \
76
76
  "mode – #{config.mode}, target – #{config.target}"
77
77
 
78
78
  at_exit { dump("total") } if config.suite?
@@ -125,7 +125,7 @@ module TestProf
125
125
 
126
126
  def build_path(name)
127
127
  TestProf.artifact_path(
128
- "stack-prof-report-#{config.mode}#{config.raw ? "-raw" : ""}-#{name}.dump"
128
+ "stack-prof-report-#{config.mode}#{"-raw" if config.raw}-#{name}.dump"
129
129
  )
130
130
  end
131
131
 
@@ -7,6 +7,7 @@ module TestProf::TagProf
7
7
  module Simple # :nodoc: all
8
8
  class << self
9
9
  include TestProf::Logging
10
+
10
11
  using TestProf::FloatDuration
11
12
 
12
13
  def dump(result)
@@ -35,7 +36,7 @@ module TestProf::TagProf
35
36
  )
36
37
  end
37
38
 
38
- header << format(
39
+ header << format( # rubocop:disable Style/RedundantFormat
39
40
  "%6s %6s %6s %12s",
40
41
  "total", "%total", "%time", "avg"
41
42
  )
@@ -8,6 +8,7 @@ module TestProf
8
8
  module Reporter
9
9
  class Text
10
10
  include Logging
11
+
11
12
  using FloatDuration
12
13
  using StringTruncate
13
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "1.4.4"
4
+ VERSION = "1.5.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-prof
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.4
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-03 00:00:00.000000000 Z
11
+ date: 2025-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -135,7 +135,7 @@ files:
135
135
  - lib/test_prof/before_all.rb
136
136
  - lib/test_prof/before_all/adapters/active_record.rb
137
137
  - lib/test_prof/before_all/isolator.rb
138
- - lib/test_prof/cops/inject.rb
138
+ - lib/test_prof/cops/plugin.rb
139
139
  - lib/test_prof/cops/rspec/aggregate_examples.rb
140
140
  - lib/test_prof/cops/rspec/aggregate_examples/its.rb
141
141
  - lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb
@@ -241,7 +241,8 @@ metadata:
241
241
  homepage_uri: https://test-prof.evilmartians.io/
242
242
  source_code_uri: https://github.com/test-prof/test-prof
243
243
  funding_uri: https://github.com/sponsors/test-prof
244
- post_install_message:
244
+ default_lint_roller_plugin: RuboCop::TestProf::Plugin
245
+ post_install_message:
245
246
  rdoc_options: []
246
247
  require_paths:
247
248
  - lib
@@ -257,7 +258,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
258
  version: '0'
258
259
  requirements: []
259
260
  rubygems_version: 3.4.19
260
- signing_key:
261
+ signing_key:
261
262
  specification_version: 4
262
263
  summary: Ruby applications tests profiling tools
263
264
  test_files: []
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This is shamelessly borrowed from RuboCop RSpec
4
- # https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
5
- module TestProf
6
- module Cops
7
- # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
8
- # bit of our configuration.
9
- module Inject
10
- PROJECT_ROOT = Pathname.new(__dir__).parent.parent.parent.expand_path.freeze
11
- CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
12
-
13
- def self.defaults!
14
- path = CONFIG_DEFAULT.to_s
15
- hash = RuboCop::ConfigLoader.send(:load_yaml_configuration, path)
16
- config = RuboCop::Config.new(hash, path)
17
- puts "configuration from #{path}" if RuboCop::ConfigLoader.debug?
18
- config = RuboCop::ConfigLoader.merge_with_default(config, path)
19
- RuboCop::ConfigLoader.instance_variable_set(:@default_configuration, config)
20
- end
21
- end
22
- end
23
- end
24
-
25
- TestProf::Cops::Inject.defaults!