test-prof 1.4.0 → 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/lib/test_prof/any_fixture/dsl.rb +16 -4
  4. data/lib/test_prof/any_fixture/dump.rb +1 -1
  5. data/lib/test_prof/before_all/adapters/active_record.rb +31 -0
  6. data/lib/test_prof/before_all.rb +30 -7
  7. data/lib/test_prof/cops/plugin.rb +31 -0
  8. data/lib/test_prof/cops/rspec/aggregate_examples/its.rb +2 -2
  9. data/lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb +1 -1
  10. data/lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb +1 -1
  11. data/lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb +1 -1
  12. data/lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb +1 -1
  13. data/lib/test_prof/cops/rspec/aggregate_examples.rb +12 -18
  14. data/lib/test_prof/core.rb +4 -0
  15. data/lib/test_prof/event_prof/custom_events/factory_create.rb +1 -1
  16. data/lib/test_prof/event_prof/rspec.rb +1 -0
  17. data/lib/test_prof/ext/active_record_refind.rb +1 -1
  18. data/lib/test_prof/ext/string_truncate.rb +1 -1
  19. data/lib/test_prof/factory_all_stub.rb +1 -1
  20. data/lib/test_prof/factory_bot.rb +3 -7
  21. data/lib/test_prof/factory_doctor/rspec.rb +1 -0
  22. data/lib/test_prof/factory_doctor.rb +3 -3
  23. data/lib/test_prof/factory_prof/factory_builders/factory_bot.rb +1 -1
  24. data/lib/test_prof/factory_prof/printers/simple.rb +35 -5
  25. data/lib/test_prof/factory_prof.rb +5 -3
  26. data/lib/test_prof/memory_prof/printer.rb +1 -0
  27. data/lib/test_prof/recipes/logging.rb +20 -0
  28. data/lib/test_prof/recipes/minitest/before_all.rb +1 -1
  29. data/lib/test_prof/rspec_dissect/rspec.rb +1 -0
  30. data/lib/test_prof/rspec_stamp/parser.rb +1 -1
  31. data/lib/test_prof/rspec_stamp.rb +7 -5
  32. data/lib/test_prof/rubocop.rb +1 -1
  33. data/lib/test_prof/stack_prof.rb +2 -2
  34. data/lib/test_prof/tag_prof/printers/simple.rb +2 -1
  35. data/lib/test_prof/tps_prof/reporter/text.rb +1 -0
  36. data/lib/test_prof/version.rb +1 -1
  37. metadata +7 -6
  38. data/lib/test_prof/cops/inject.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6636a3c69af7cd067079bd1e91ef72d38b650a372a13e6363c3de9e510a8970b
4
- data.tar.gz: 2f1a503a8adcf4af85cad6b98ab4682394c5222b1f5290cc6aaeec97da77ef06
3
+ metadata.gz: 119118a5840991b0d1bde28e49aca9f3f72f77f89ba57704c53ce2e8a023ad06
4
+ data.tar.gz: 6f124770cfeba6346f744dbf66a00b118c6c207711af9f18977abea153de4f7a
5
5
  SHA512:
6
- metadata.gz: 68701c8c91b20f810ff93f3e3360d4f8f7b3210a2014f3a4463542b0e31b657ab3093da4eaf4b2bf9cee59d1e99ded9ef51d83e7a27855f05113f681219f7888
7
- data.tar.gz: 438a4297ebff65d6f9d5aec752775a638f5c6255c63d439f278eda596825595c2b6d74cc609057c42712247a98eefb07c42dcd1133d4d1ae5162898a2af5068c
6
+ metadata.gz: 6827f8b914ce78da9cabff0dd04ef643c3571e22864a1c6c215028fd4a2e9f79dbbab0d69e7ac9184753460aee96a2202a9201f7bdf2177074b42de47e893ac4
7
+ data.tar.gz: 4beacf5a982c0fe64c1c363660f2985be9dd6470ac847eb10e5ef4cd3ddf3360681e8c09fb511d5a99418629b455755e93824768c21520d0c754a908a7e7080c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
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
+
29
+ ## 1.4.4 (2025-01-03)
30
+
31
+ - Fix _stamping_ specs with single quotes with RSpec Stamp. ([@elasticspoon][])
32
+
33
+ ## 1.4.3 (2024-12-18)
34
+
35
+ - Fix handling new (lazy) connection pools in `before_all`. ([@palkan][])
36
+
37
+ - Updates Rubocop::Cop code to comply with most modern API. ([@aseroff][])
38
+
39
+ ## 1.4.2 (2024-09-03) 🗓️
40
+
41
+ - Ignore default scopes in `ActiveRecord::Base#refind`. ([@palkan][])
42
+
43
+ ## 1.4.1 (2024-08-23)
44
+
45
+ - Skips loading the ActiveRecord adapter for runs where RSpec --dry-run mode is enabled. ([@devinburnette][])
46
+
5
47
  ## 1.4.0 (2024-08-12)
6
48
 
7
49
  - AnyFixture: Disable fixtures cache within _clean fixture_ context. Automatically _refind_ records when using `#fixture`. ([@palkan][])
@@ -434,3 +476,5 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
434
476
  [@lioneldebauge]: https://github.com/lioneldebauge
435
477
  [@lHydra]: https://github.com/lHydra
436
478
  [@john-h-k]: https://github.com/john-h-k
479
+ [@devinburnette]: https://github.com/devinburnette
480
+ [@elasticspoon]: https://github.com/elasticspoon
@@ -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
@@ -69,7 +69,7 @@ module TestProf
69
69
  end
70
70
 
71
71
  def commit
72
- return unless defined?(:@file)
72
+ return unless instance_variable_defined?(:@file)
73
73
 
74
74
  file.close
75
75
 
@@ -10,6 +10,7 @@ module TestProf
10
10
  class << self
11
11
  if ::ActiveRecord::Base.connection.pool.respond_to?(:pin_connection!)
12
12
  def begin_transaction
13
+ subscribe!
13
14
  ::ActiveRecord::Base.connection_handler.connection_pool_list(:writing).each do |pool|
14
15
  pool.pin_connection!(true)
15
16
  end
@@ -19,6 +20,36 @@ module TestProf
19
20
  ::ActiveRecord::Base.connection_handler.connection_pool_list(:writing).each do |pool|
20
21
  pool.unpin_connection!
21
22
  end
23
+ unsubscribe!
24
+ end
25
+
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
+
32
+ Thread.current[:before_all_connection_subscriber] = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
33
+ connection_name = payload[:connection_name] if payload.key?(:connection_name)
34
+ shard = payload[:shard] if payload.key?(:shard)
35
+ next unless connection_name
36
+
37
+ pool = ::ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
38
+ next unless pool && pool.role == :writing
39
+
40
+ pool.pin_connection!(true)
41
+ end
42
+ end
43
+
44
+ def unsubscribe!
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
+
51
+ ActiveSupport::Notifications.unsubscribe(Thread.current[:before_all_connection_subscriber])
52
+ Thread.current[:before_all_connection_subscriber] = nil
22
53
  end
23
54
  else
24
55
  def all_connections
@@ -14,8 +14,26 @@ module TestProf
14
14
  end
15
15
  end
16
16
 
17
+ # Used in dry-run mode
18
+ class NoopAdapter
19
+ class << self
20
+ def begin_transaction(...)
21
+ end
22
+
23
+ def rollback_transaction(...)
24
+ end
25
+
26
+ def setup_fixtures(...)
27
+ end
28
+ end
29
+ end
30
+
17
31
  class << self
18
- attr_accessor :adapter
32
+ attr_writer :adapter
33
+
34
+ def adapter
35
+ @adapter ||= default_adapter
36
+ end
19
37
 
20
38
  def begin_transaction(scope = nil, metadata = [])
21
39
  raise AdapterMissing if adapter.nil?
@@ -47,6 +65,17 @@ module TestProf
47
65
  def configure
48
66
  yield config
49
67
  end
68
+
69
+ private
70
+
71
+ def default_adapter
72
+ return NoopAdapter if TestProf.dry_run?
73
+
74
+ if defined?(::ActiveRecord::Base)
75
+ require "test_prof/before_all/adapters/active_record"
76
+ Adapters::ActiveRecord
77
+ end
78
+ end
50
79
  end
51
80
 
52
81
  class HookEntry # :nodoc:
@@ -138,12 +167,6 @@ module TestProf
138
167
  end
139
168
  end
140
169
 
141
- if defined?(::ActiveRecord::Base)
142
- require "test_prof/before_all/adapters/active_record"
143
-
144
- TestProf::BeforeAll.adapter = TestProf::BeforeAll::Adapters::ActiveRecord
145
- end
146
-
147
170
  if defined?(::Isolator)
148
171
  require "test_prof/before_all/isolator"
149
172
  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
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < ::RuboCop::Cop::Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Base
7
7
  # @example `its`
8
8
  #
9
9
  # # Supports regular `its` call with an attribute/method name,
@@ -64,7 +64,7 @@ module RuboCop
64
64
  return super unless its?(example.send_node)
65
65
 
66
66
  # First parameter to `its` is not metadata.
67
- example.send_node.arguments[1..-1]
67
+ example.send_node.arguments[1..]
68
68
  end
69
69
 
70
70
  def its?(node)
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < ::RuboCop::Cop::Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Base
7
7
  # @internal Support methods for keeping newlines around examples.
8
8
  module LineRangeHelpers
9
9
  include RangeHelp
@@ -5,7 +5,7 @@ require "test_prof/cops/rspec/language"
5
5
  module RuboCop
6
6
  module Cop
7
7
  module RSpec
8
- class AggregateExamples < ::RuboCop::Cop::Cop
8
+ class AggregateExamples < ::RuboCop::Cop::Base
9
9
  # When aggregated, the expectations will fail when not supposed to or
10
10
  # have a risk of not failing when expected to. One example is
11
11
  # `validate_presence_of :comment` as it leaves an empty comment after
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < ::RuboCop::Cop::Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Base
7
7
  # @internal
8
8
  # Support methods for example metadata.
9
9
  # Examples with similar metadata are grouped.
@@ -5,7 +5,7 @@ require "test_prof/cops/rspec/language"
5
5
  module RuboCop
6
6
  module Cop
7
7
  module RSpec
8
- class AggregateExamples < ::RuboCop::Cop::Cop
8
+ class AggregateExamples < ::RuboCop::Cop::Base
9
9
  # @internal
10
10
  # Node matchers and searchers.
11
11
  module NodeMatchers
@@ -108,7 +108,8 @@ module RuboCop
108
108
  # expect(number).to be_odd
109
109
  # end
110
110
  #
111
- class AggregateExamples < ::RuboCop::Cop::Cop
111
+ class AggregateExamples < ::RuboCop::Cop::Base
112
+ extend AutoCorrector
112
113
  include LineRangeHelpers
113
114
  include MetadataHelpers
114
115
  include NodeMatchers
@@ -123,29 +124,22 @@ module RuboCop
123
124
  def on_block(node)
124
125
  example_group_with_several_examples(node) do |all_examples|
125
126
  example_clusters(all_examples).each do |_, examples|
126
- examples[1..-1].each do |example|
127
+ examples.drop(1).each do |example|
127
128
  add_offense(example,
128
- location: :expression,
129
- message: message_for(example, examples[0]))
129
+ message: message_for(example, examples[0])) do |corrector|
130
+ clusters = example_clusters_for_autocorrect(example)
131
+ clusters.each do |metadata, examples|
132
+ range = range_for_replace(examples)
133
+ replacement = aggregated_example(examples, metadata)
134
+ corrector.replace(range, replacement)
135
+ examples.drop(1).map { |example| drop_example(corrector, example) }
136
+ end
137
+ end
130
138
  end
131
139
  end
132
140
  end
133
141
  end
134
142
 
135
- def autocorrect(example_node)
136
- clusters = example_clusters_for_autocorrect(example_node)
137
- return if clusters.empty?
138
-
139
- lambda do |corrector|
140
- clusters.each do |metadata, examples|
141
- range = range_for_replace(examples)
142
- replacement = aggregated_example(examples, metadata)
143
- corrector.replace(range, replacement)
144
- examples[1..-1].map { |example| drop_example(corrector, example) }
145
- end
146
- end
147
- end
148
-
149
143
  private
150
144
 
151
145
  # Clusters of examples in the same example group, on the same nesting
@@ -75,6 +75,10 @@ module TestProf
75
75
  defined?(::Spring::Application) && (disabled.nil? || disabled.empty? || disabled == "0")
76
76
  end
77
77
 
78
+ def dry_run?
79
+ rspec? && ::RSpec.configuration.dry_run?
80
+ end
81
+
78
82
  # Returns the current process time
79
83
  def now
80
84
  Process.clock_gettime_for_test_prof(Process::CLOCK_MONOTONIC)
@@ -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
  # We need it to make sure that the state is clean.
14
14
  def refind
15
- self.class.find(send(self.class.primary_key))
15
+ self.class.unscoped.find(send(self.class.primary_key))
16
16
  end
17
17
  end
18
18
  end
@@ -12,7 +12,7 @@ module TestProf
12
12
  head = ((limit - 3) / 2)
13
13
  tail = head + 3 - limit
14
14
 
15
- "#{self[0..(head - 1)]}...#{self[tail..-1]}"
15
+ "#{self[0..(head - 1)]}...#{self[tail..]}"
16
16
  end
17
17
  end
18
18
  end
@@ -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
4
+ TestProf.require("active_support")
5
5
 
6
- TestProf.require("active_support/inflector")
7
-
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
 
@@ -105,10 +106,9 @@ module TestProf
105
106
  # Do not analyze code within the block
106
107
  def ignore
107
108
  @ignored = true
108
- res = yield
109
+ yield
109
110
  ensure
110
111
  @ignored = false
111
- res
112
112
  end
113
113
 
114
114
  def ignore!
@@ -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
@@ -104,7 +104,7 @@ module TestProf
104
104
  if base.respond_to?(:parallelize_teardown)
105
105
  base.parallelize_teardown do
106
106
  last_klass = ::Minitest.previous_klass
107
- if last_klass&.respond_to?(:parallelized) && last_klass&.parallelized
107
+ if last_klass&.respond_to?(:parallelized) && last_klass.parallelized
108
108
  last_klass.before_all_executor&.deactivate!
109
109
  end
110
110
  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[
@@ -143,7 +143,7 @@ module TestProf
143
143
  elsif expr.first == :@const
144
144
  expr[1]
145
145
  elsif expr.first == :const_path_ref
146
- expr[1..-1].map(&method(:parse_const)).join("::")
146
+ expr[1..].map(&method(:parse_const)).join("::")
147
147
  end
148
148
  end
149
149
  end
@@ -153,14 +153,16 @@ module TestProf
153
153
  end
154
154
  end
155
155
 
156
- replacement = "\\1#{parsed.fname}#{need_parens ? "(" : " "}" \
157
- "#{[desc, tags_str, htags_str].compact.join(", ")}" \
158
- "#{need_parens ? ") " : " "}\\3"
156
+ replacement = proc do
157
+ $1 + "#{parsed.fname}#{need_parens ? "(" : " "}" \
158
+ "#{[desc, tags_str, htags_str].compact.join(", ")}" \
159
+ "#{need_parens ? ") " : " "}" + $3
160
+ end
159
161
 
160
162
  if config.dry_run?
161
- log :info, "Patched: #{example.sub(EXAMPLE_RXP, replacement)}"
163
+ log :info, "Patched: #{example.sub(EXAMPLE_RXP, &replacement)}"
162
164
  else
163
- example.sub!(EXAMPLE_RXP, replacement)
165
+ example.sub!(EXAMPLE_RXP, &replacement)
164
166
  end
165
167
  true
166
168
  end
@@ -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.0"
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.0
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: 2024-08-12 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!