rspec-tracer 1.2.3 → 2.0.0.pre.2

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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +384 -67
  3. data/README.md +454 -429
  4. data/bin/rspec-tracer +15 -0
  5. data/lib/rspec_tracer/cache/Rakefile +43 -0
  6. data/lib/rspec_tracer/cli/cache_clear.rb +111 -0
  7. data/lib/rspec_tracer/cli/cache_info.rb +104 -0
  8. data/lib/rspec_tracer/cli/doctor.rb +284 -0
  9. data/lib/rspec_tracer/cli/explain.rb +158 -0
  10. data/lib/rspec_tracer/cli/report_open.rb +82 -0
  11. data/lib/rspec_tracer/cli.rb +116 -0
  12. data/lib/rspec_tracer/configuration.rb +1196 -3
  13. data/lib/rspec_tracer/engine.rb +1168 -0
  14. data/lib/rspec_tracer/example.rb +141 -11
  15. data/lib/rspec_tracer/filter.rb +35 -0
  16. data/lib/rspec_tracer/line_stub.rb +61 -0
  17. data/lib/rspec_tracer/load_config.rb +2 -2
  18. data/lib/rspec_tracer/logger.rb +15 -0
  19. data/lib/rspec_tracer/rails/README.md +78 -0
  20. data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
  21. data/lib/rspec_tracer/rails/notifications.rb +263 -0
  22. data/lib/rspec_tracer/rails/preset.rb +94 -0
  23. data/lib/rspec_tracer/rails/railtie.rb +22 -0
  24. data/lib/rspec_tracer/rails.rb +15 -0
  25. data/lib/rspec_tracer/remote_cache/README.md +140 -0
  26. data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
  27. data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
  28. data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
  29. data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
  30. data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
  31. data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
  32. data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
  33. data/lib/rspec_tracer/remote_cache/user_tasks.rb +436 -0
  34. data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
  35. data/lib/rspec_tracer/remote_cache.rb +22 -0
  36. data/lib/rspec_tracer/reporters/README.md +103 -0
  37. data/lib/rspec_tracer/reporters/base.rb +87 -0
  38. data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
  39. data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
  40. data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
  41. data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
  42. data/lib/rspec_tracer/reporters/html/README.md +80 -0
  43. data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
  44. data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
  45. data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
  46. data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
  47. data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
  48. data/lib/rspec_tracer/reporters/html/package.json +29 -0
  49. data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
  50. data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
  51. data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
  52. data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
  53. data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
  54. data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
  55. data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
  56. data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
  57. data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
  58. data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
  59. data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
  60. data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
  61. data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
  62. data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
  63. data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
  64. data/lib/rspec_tracer/reporters/registry.rb +120 -0
  65. data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
  66. data/lib/rspec_tracer/rspec/README.md +73 -0
  67. data/lib/rspec_tracer/rspec/installation.rb +97 -0
  68. data/lib/rspec_tracer/rspec/metadata.rb +96 -0
  69. data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
  70. data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
  71. data/lib/rspec_tracer/rspec/runner_hook.rb +239 -0
  72. data/lib/rspec_tracer/source_file.rb +24 -7
  73. data/lib/rspec_tracer/storage/README.md +35 -0
  74. data/lib/rspec_tracer/storage/backend.rb +130 -0
  75. data/lib/rspec_tracer/storage/json_backend.rb +884 -0
  76. data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
  77. data/lib/rspec_tracer/storage/schema.rb +50 -0
  78. data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
  79. data/lib/rspec_tracer/storage/serializer/msgpack.rb +167 -0
  80. data/lib/rspec_tracer/storage/snapshot.rb +141 -0
  81. data/lib/rspec_tracer/storage/sqlite_backend.rb +693 -0
  82. data/lib/rspec_tracer/time_formatter.rb +37 -18
  83. data/lib/rspec_tracer/tracker/README.md +36 -0
  84. data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
  85. data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
  86. data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
  87. data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
  88. data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
  89. data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
  90. data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
  91. data/lib/rspec_tracer/tracker/filter.rb +127 -0
  92. data/lib/rspec_tracer/tracker/input.rb +99 -0
  93. data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
  94. data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
  95. data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
  96. data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
  97. data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
  98. data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
  99. data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
  100. data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
  101. data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
  102. data/lib/rspec_tracer/version.rb +4 -1
  103. data/lib/rspec_tracer.rb +231 -491
  104. metadata +94 -43
  105. data/lib/rspec_tracer/cache.rb +0 -207
  106. data/lib/rspec_tracer/coverage_merger.rb +0 -42
  107. data/lib/rspec_tracer/coverage_reporter.rb +0 -187
  108. data/lib/rspec_tracer/coverage_writer.rb +0 -58
  109. data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
  110. data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
  111. data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
  112. data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
  113. data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
  114. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
  115. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
  116. data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
  117. data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
  118. data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
  119. data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
  120. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
  121. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
  122. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
  123. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
  124. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
  125. data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
  126. data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
  127. data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
  128. data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
  129. data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
  130. data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
  131. data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
  132. data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
  133. data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
  134. data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
  135. data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
  136. data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
  137. data/lib/rspec_tracer/report_generator.rb +0 -158
  138. data/lib/rspec_tracer/report_merger.rb +0 -68
  139. data/lib/rspec_tracer/report_writer.rb +0 -141
  140. data/lib/rspec_tracer/reporter.rb +0 -204
  141. data/lib/rspec_tracer/rspec_reporter.rb +0 -41
  142. data/lib/rspec_tracer/rspec_runner.rb +0 -56
  143. data/lib/rspec_tracer/ruby_coverage.rb +0 -9
  144. data/lib/rspec_tracer/runner.rb +0 -278
data/bin/rspec-tracer ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Entry point for the `rspec-tracer` CLI. The binary is opt-in for
5
+ # users who want diagnostics, cache inspection, or report-opening
6
+ # from the command line; the canonical CI flow continues to go
7
+ # through `rake rspec_tracer:remote_cache:*` tasks per
8
+ # USER_FACING_SURFACE.md §5.
9
+
10
+ lib_dir = File.expand_path('../lib', __dir__)
11
+ $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'rspec_tracer/cli'
14
+
15
+ exit RSpecTracer::CLI.run(ARGV)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # User-facing Rake task shim for local-cache maintenance. Load from
4
+ # the user's own Rakefile when they want one-off cleanup:
5
+ #
6
+ # spec = Gem::Specification.find_by_name('rspec-tracer')
7
+ # load "#{spec.gem_dir}/lib/rspec_tracer/cache/Rakefile"
8
+ #
9
+ # Defines `rspec_tracer:cache:gc` which prunes old run-id
10
+ # directories under the local cache while retaining the `N`
11
+ # most-recent configured via `cache_retention_local_count` (default
12
+ # 5). Prune-on-save already handles the steady-state cleanup; this
13
+ # task is for catching up after retention was opted-out-of and the
14
+ # cache grew beyond the configured cap.
15
+
16
+ require 'rspec_tracer'
17
+ require 'rspec_tracer/storage/json_backend'
18
+
19
+ namespace :rspec_tracer do
20
+ namespace :cache do
21
+ desc 'Prune old run-id directories under the local cache ' \
22
+ '(respects cache_retention_local_count)'
23
+ task :gc do
24
+ count = RSpecTracer.cache_retention_local_count
25
+ if count.nil? || count.zero?
26
+ RSpecTracer.logger.warn(
27
+ 'rspec-tracer cache gc: cache_retention_local_count is 0 (opt-out); nothing to prune'
28
+ )
29
+ next
30
+ end
31
+
32
+ backend = RSpecTracer::Storage::JsonBackend.new(
33
+ cache_path: RSpecTracer.cache_path,
34
+ logger: RSpecTracer.logger,
35
+ retention_local_count: count
36
+ )
37
+ removed = backend.prune_run_dirs!(keep: count)
38
+ RSpecTracer.logger.info(
39
+ "rspec-tracer cache gc: pruned #{removed} run-id dir(s); kept #{count} most-recent"
40
+ )
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module RSpecTracer
6
+ # Internal CLI — see {RSpecTracer} for the user-facing surface.
7
+ # @api private
8
+ module CLI
9
+ # `rspec-tracer cache:clear` — remove cache, coverage, and report
10
+ # directories. Prompts for confirmation unless `--yes` is passed.
11
+ # The next rspec run is a cold run.
12
+ module CacheClear
13
+ # @param args [Array<String>] sub-command args (`-y` / `--yes`
14
+ # skips confirmation; `-h` / `--help` prints help).
15
+ # @param stdout [IO]
16
+ # @param stderr [IO]
17
+ # @return [Integer] exit status (0 = success / aborted, 1 = error).
18
+ def self.run(args, stdout: $stdout, stderr: $stderr)
19
+ return print_help(stdout) if args.include?('-h') || args.include?('--help')
20
+
21
+ require 'rspec_tracer/load_config'
22
+ existing = existing_targets
23
+ return nothing_to_remove(stdout) if existing.empty?
24
+
25
+ announce(stdout, existing)
26
+ return aborted(stdout) unless skip_confirmation?(args) || confirm?(stdout)
27
+
28
+ remove_each(stdout, existing)
29
+ 0
30
+ rescue StandardError => e
31
+ stderr.puts "cache:clear: #{e.class}: #{e.message}"
32
+ 1
33
+ end
34
+
35
+ # Returns true when any of the documented skip-confirmation
36
+ # flags is present. `--yes` / `-y` is the canonical form;
37
+ # `--force` / `-f` is the Unix-conventional synonym accepted
38
+ # so users' muscle memory works. Either form skips the
39
+ # interactive `Proceed? [y/N]` prompt.
40
+ # @api private
41
+ SKIP_CONFIRMATION_FLAGS = %w[--yes -y --force -f].freeze
42
+
43
+ # @api private
44
+ def self.skip_confirmation?(args)
45
+ args.any? { |arg| SKIP_CONFIRMATION_FLAGS.include?(arg) }
46
+ end
47
+
48
+ # Internal helper for the tracer pipeline.
49
+ # @api private
50
+ def self.existing_targets
51
+ [RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path]
52
+ .select { |path| File.directory?(path) }
53
+ end
54
+
55
+ # Internal helper for the tracer pipeline.
56
+ # @api private
57
+ def self.nothing_to_remove(stdout)
58
+ stdout.puts 'cache:clear: nothing to remove (cache directories do not exist)'
59
+ 0
60
+ end
61
+
62
+ # Internal helper for the tracer pipeline.
63
+ # @api private
64
+ def self.announce(stdout, existing)
65
+ stdout.puts 'cache:clear: will remove:'
66
+ existing.each { |path| stdout.puts " - #{path}" }
67
+ end
68
+
69
+ # Internal helper for the tracer pipeline.
70
+ # @api private
71
+ def self.confirm?(stdout)
72
+ stdout.print 'Proceed? [y/N] '
73
+ stdout.flush
74
+ response = $stdin.gets&.chomp&.downcase
75
+ %w[y yes].include?(response)
76
+ end
77
+
78
+ # Internal helper for the tracer pipeline.
79
+ # @api private
80
+ def self.aborted(stdout)
81
+ stdout.puts 'cache:clear: aborted'
82
+ 0
83
+ end
84
+
85
+ # Internal helper for the tracer pipeline.
86
+ # @api private
87
+ def self.remove_each(stdout, existing)
88
+ existing.each do |path|
89
+ FileUtils.rm_rf(path)
90
+ stdout.puts " removed #{path}"
91
+ end
92
+ end
93
+
94
+ # Internal helper for the tracer pipeline.
95
+ # @api private
96
+ def self.print_help(stdout)
97
+ stdout.puts <<~HELP
98
+ Usage: rspec-tracer cache:clear [--yes | --force]
99
+
100
+ Remove cache, coverage, and report directories. The next rspec
101
+ run will be a cold run (full re-execution + cache rebuild).
102
+
103
+ Options:
104
+ -y, --yes Skip the confirmation prompt.
105
+ -f, --force Synonym for --yes.
106
+ HELP
107
+ 0
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_tracer/storage/backend'
4
+ require 'rspec_tracer/storage/json_backend'
5
+ require 'rspec_tracer/storage/schema'
6
+ require 'rspec_tracer/storage/sqlite_backend' if RUBY_ENGINE == 'ruby'
7
+
8
+ module RSpecTracer
9
+ # Internal CLI — see {RSpecTracer} for the user-facing surface.
10
+ # @api private
11
+ module CLI
12
+ # `rspec-tracer cache:info` — show cache size, last run, and
13
+ # invalidation stats. Backend-agnostic: dispatches through
14
+ # {RSpecTracer::Storage::Backend.build} so `storage_backend
15
+ # :sqlite` reports the populated cache instead of the false
16
+ # "no cache yet" the JsonBackend-only path used to emit.
17
+ module CacheInfo
18
+ # @param args [Array<String>] sub-command args (`-h` / `--help`).
19
+ # @param stdout [IO]
20
+ # @param stderr [IO]
21
+ # @return [Integer] exit status (0 = success).
22
+ def self.run(args, stdout: $stdout, stderr: $stderr)
23
+ return print_help(stdout) if args.include?('-h') || args.include?('--help')
24
+
25
+ require 'rspec_tracer/load_config'
26
+
27
+ cache_path = RSpecTracer.cache_path
28
+ stdout.puts "cache_path: #{cache_path}"
29
+ stdout.puts "size: #{format_bytes(directory_size(cache_path))}"
30
+
31
+ backend = Storage::Backend.build(cache_path: cache_path, configuration: RSpecTracer)
32
+ run_id = backend.last_run_id
33
+ if run_id.nil? || run_id.to_s.empty?
34
+ stdout.puts 'last_run: no cache yet (run rspec first)'
35
+ return 0
36
+ end
37
+
38
+ stdout.puts "last_run: #{run_id}"
39
+ print_example_count(stdout, backend)
40
+ 0
41
+ rescue StandardError => e
42
+ stderr.puts "cache:info: #{e.class}: #{e.message}"
43
+ 1
44
+ end
45
+
46
+ # Internal helper for the tracer pipeline.
47
+ # @api private
48
+ def self.print_help(stdout)
49
+ stdout.puts <<~HELP
50
+ Usage: rspec-tracer cache:info
51
+
52
+ Show the on-disk cache size, the last run id, and example counts
53
+ for the most recent run. Backend-aware: works under
54
+ `storage_backend :json` (default) and `storage_backend :sqlite`.
55
+ Read-only; does not modify the cache.
56
+ HELP
57
+ 0
58
+ end
59
+
60
+ # Internal helper for the tracer pipeline.
61
+ # @api private
62
+ def self.print_example_count(stdout, backend)
63
+ snapshot = backend.load_graph(schema_version: Storage::Schema::CURRENT)
64
+ if snapshot.nil?
65
+ stdout.puts 'examples: <unknown> (schema mismatch; next rspec run will be cold)'
66
+ return
67
+ end
68
+
69
+ stdout.puts "examples: #{snapshot.all_examples.size} tracked"
70
+ end
71
+
72
+ # Internal helper for the tracer pipeline.
73
+ # @api private
74
+ def self.directory_size(path)
75
+ return 0 unless File.directory?(path)
76
+
77
+ total = 0
78
+ Dir.glob(File.join(path, '**', '*'), File::FNM_DOTMATCH).each do |entry|
79
+ next unless File.file?(entry)
80
+
81
+ total += File.size(entry)
82
+ rescue SystemCallError
83
+ next
84
+ end
85
+ total
86
+ end
87
+
88
+ # Internal helper for the tracer pipeline.
89
+ # @api private
90
+ def self.format_bytes(bytes)
91
+ return '0 B' if bytes <= 0
92
+
93
+ units = %w[B KB MB GB]
94
+ scale = bytes
95
+ unit_index = 0
96
+ while scale >= 1024 && unit_index < units.length - 1
97
+ scale /= 1024.0
98
+ unit_index += 1
99
+ end
100
+ format('%<scale>.1f %<unit>s', scale: scale, unit: units[unit_index])
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ # Internal CLI — see {RSpecTracer} for the user-facing surface.
5
+ # @api private
6
+ module CLI
7
+ # `rspec-tracer doctor` — diagnose config + environment.
8
+ # Reports Ruby + rspec-tracer versions, project root resolution,
9
+ # cache / coverage / report directory state, and SimpleCov / Rails
10
+ # presence. Exits 0 on healthy diagnosis, 1 if any check fails.
11
+ module Doctor
12
+ # @param args [Array<String>] sub-command args (`-h` / `--help`).
13
+ # @param stdout [IO]
14
+ # @param stderr [IO]
15
+ # @return [Integer] exit status (0 = healthy, 1 = any check
16
+ # FAILed; warnings keep status 0).
17
+ def self.run(args, stdout: $stdout, stderr: $stderr)
18
+ return print_help(stdout) if args.include?('-h') || args.include?('--help')
19
+
20
+ require 'rspec_tracer/load_config'
21
+
22
+ checks = [
23
+ ruby_version_check,
24
+ tracer_version_check,
25
+ project_root_check,
26
+ cache_path_check,
27
+ coverage_path_check,
28
+ report_path_check,
29
+ git_check,
30
+ simplecov_check,
31
+ rails_check,
32
+ cache_schema_version_check,
33
+ remote_cache_check,
34
+ ar_schema_narrow_attribution_check
35
+ ]
36
+ checks.each { |line| stdout.puts line }
37
+ ok = checks.none? { |line| line.start_with?('FAIL') }
38
+ ok ? 0 : 1
39
+ rescue StandardError => e
40
+ stderr.puts "doctor: #{e.class}: #{e.message}"
41
+ 1
42
+ end
43
+
44
+ # Internal helper for the tracer pipeline.
45
+ # @api private
46
+ def self.print_help(stdout)
47
+ stdout.puts <<~HELP
48
+ Usage: rspec-tracer doctor
49
+
50
+ Diagnose rspec-tracer config and environment. Prints a checklist
51
+ of versions, paths, and integrations; exits 0 if all checks pass.
52
+ HELP
53
+ 0
54
+ end
55
+
56
+ # Internal helper for the tracer pipeline.
57
+ # @api private
58
+ def self.ruby_version_check
59
+ "OK ruby: #{RUBY_DESCRIPTION}"
60
+ end
61
+
62
+ # Internal helper for the tracer pipeline.
63
+ # @api private
64
+ def self.tracer_version_check
65
+ "OK rspec-tracer: #{RSpecTracer::VERSION}"
66
+ end
67
+
68
+ # Internal helper for the tracer pipeline.
69
+ # @api private
70
+ def self.project_root_check
71
+ "OK root: #{RSpecTracer.root}"
72
+ end
73
+
74
+ # Internal helper for the tracer pipeline.
75
+ # @api private
76
+ def self.cache_path_check
77
+ path_check('cache_path:', RSpecTracer.cache_path)
78
+ end
79
+
80
+ # Internal helper for the tracer pipeline.
81
+ # @api private
82
+ def self.coverage_path_check
83
+ path_check('coverage_path:', RSpecTracer.coverage_path)
84
+ end
85
+
86
+ # Internal helper for the tracer pipeline.
87
+ # @api private
88
+ def self.report_path_check
89
+ path_check('report_path:', RSpecTracer.report_path)
90
+ end
91
+
92
+ # Internal helper for the tracer pipeline.
93
+ # @api private
94
+ def self.path_check(label, path)
95
+ return "FAIL #{label} <missing>" if path.nil? || path.empty?
96
+ return "FAIL #{label} #{path} (does not exist)" unless File.directory?(path)
97
+ return "FAIL #{label} #{path} (not writable)" unless File.writable?(path)
98
+
99
+ "OK #{label} #{path}"
100
+ end
101
+
102
+ # Internal helper for the tracer pipeline.
103
+ # @api private
104
+ def self.git_check
105
+ if system('git', 'rev-parse', 'HEAD', out: File::NULL, err: File::NULL)
106
+ 'OK git: HEAD reachable (remote_cache will work)'
107
+ else
108
+ 'WARN git: not in a git repo (remote_cache will degrade gracefully)'
109
+ end
110
+ end
111
+
112
+ # `bundle exec rspec-tracer doctor` runs in its own process via
113
+ # the gem's `bin/rspec-tracer` binstub, NOT inside the user's
114
+ # rspec boot — so app code never loads here and a bare
115
+ # `defined?(::SimpleCov)` check would falsely report "not
116
+ # loaded" on projects that DO have SimpleCov in their
117
+ # Gemfile. Probe `Gem.loaded_specs` first to surface the
118
+ # "installed but not loaded in doctor's process" case
119
+ # separately from "actually not installed."
120
+ def self.simplecov_check
121
+ return 'OK SimpleCov: loaded (interop active)' if defined?(::SimpleCov)
122
+
123
+ spec = Gem.loaded_specs['simplecov']
124
+ return "INFO SimpleCov: installed (v#{spec.version}; not loaded in doctor's process)" if spec
125
+
126
+ 'INFO SimpleCov: not installed (this is fine; SimpleCov is optional)'
127
+ end
128
+
129
+ # See {.simplecov_check} for the doctor-runs-in-its-own-
130
+ # process rationale. Same three-state probe shape: loaded in
131
+ # this process / installed but not loaded / not installed.
132
+ def self.rails_check
133
+ return "OK Rails: #{::Rails::VERSION::STRING}" if defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
134
+
135
+ spec = Gem.loaded_specs['rails']
136
+ return "INFO Rails: installed (v#{spec.version}; not loaded in doctor's process)" if spec
137
+
138
+ 'INFO Rails: not installed (this is fine for non-Rails projects)'
139
+ end
140
+
141
+ # Surface a 1.x->2.0 cache mismatch BEFORE the user runs
142
+ # rspec and watches everything re-run mysteriously. Reads the
143
+ # cached `last_run.json` (if any) and compares its
144
+ # `schema_version` against the gem's `Schema::CURRENT`. Three
145
+ # outcomes: no cache yet (INFO), match (OK), or mismatch (WARN
146
+ # with the cold-run note). Never FAIL - schema mismatches are
147
+ # the documented cold-run path, not a hard error.
148
+ def self.cache_schema_version_check
149
+ require 'rspec_tracer/storage/schema'
150
+ require 'json'
151
+
152
+ cache_path = RSpecTracer.cache_path
153
+ last_run_path = File.join(cache_path.to_s, 'last_run.json')
154
+ unless File.file?(last_run_path)
155
+ return 'INFO schema: no cache yet (next rspec run is cold; expected on first install)'
156
+ end
157
+
158
+ manifest = JSON.parse(File.read(last_run_path, encoding: 'UTF-8'))
159
+ stored = manifest['schema_version']
160
+ current = RSpecTracer::Storage::Schema::CURRENT
161
+ if stored == current
162
+ "OK schema: v#{current} (matches gem)"
163
+ else
164
+ "WARN schema: stored v#{stored.inspect} != gem v#{current} " \
165
+ '(next rspec run is a cold run; expected on 1.x->2.0 upgrade)'
166
+ end
167
+ rescue StandardError => e
168
+ "WARN schema: could not read cache manifest: #{e.class}: #{e.message}"
169
+ end
170
+
171
+ # When remote_cache is configured, verify the backend is
172
+ # reachable from doctor's vantage point so the user catches
173
+ # misconfig (typo'd S3 path / unreachable Redis URL / unwritable
174
+ # local-fs dir) BEFORE the next CI run fails. Best-effort: never
175
+ # FAIL the gate, just surface a WARN/INFO line.
176
+ REMOTE_CACHE_PROBES = {
177
+ s3: ->(opts) { remote_cache_s3_check(opts) },
178
+ local_fs: ->(opts) { remote_cache_local_fs_check(opts) },
179
+ redis: ->(opts) { remote_cache_redis_check(opts) }
180
+ }.freeze
181
+
182
+ # Internal helper for the tracer pipeline.
183
+ # @api private
184
+ def self.remote_cache_check
185
+ entry = remote_cache_entry
186
+ return 'INFO remote_cache: not configured (skip)' unless entry
187
+
188
+ backend, opts = entry
189
+ probe = REMOTE_CACHE_PROBES[backend]
190
+ return "INFO remote_cache: custom backend #{backend.inspect} (skipping reachability probe)" if probe.nil?
191
+
192
+ instance_exec(opts, &probe)
193
+ end
194
+
195
+ # Internal helper for the tracer pipeline.
196
+ # @api private
197
+ def self.remote_cache_entry
198
+ return nil unless RSpecTracer.respond_to?(:remote_cache_backend_entry)
199
+
200
+ RSpecTracer.remote_cache_backend_entry
201
+ end
202
+
203
+ # Internal helper for the tracer pipeline.
204
+ # @api private
205
+ def self.remote_cache_s3_check(opts)
206
+ bucket = opts[:bucket] || opts['bucket']
207
+ return 'WARN remote_cache: :s3 configured without :bucket' if bucket.nil? || bucket.empty?
208
+
209
+ "OK remote_cache: :s3 bucket=#{bucket} " \
210
+ '(reachability not probed locally; verified end-to-end on next CI run)'
211
+ end
212
+
213
+ # Internal helper for the tracer pipeline.
214
+ # @api private
215
+ def self.remote_cache_local_fs_check(opts)
216
+ path = opts[:path] || opts['path']
217
+ return 'WARN remote_cache: :local_fs configured without :path' if path.nil? || path.empty?
218
+ return "WARN remote_cache: :local_fs path #{path} does not exist" unless File.directory?(path)
219
+ return "WARN remote_cache: :local_fs path #{path} not writable" unless File.writable?(path)
220
+
221
+ "OK remote_cache: :local_fs path=#{path}"
222
+ end
223
+
224
+ # Internal helper for the tracer pipeline.
225
+ # @api private
226
+ def self.remote_cache_redis_check(opts)
227
+ url = opts[:url] || opts['url'] || ENV.fetch('RSPEC_TRACER_REMOTE_CACHE_URI', nil)
228
+ return 'WARN remote_cache: :redis configured without :url' if url.nil? || url.empty?
229
+
230
+ "OK remote_cache: :redis url=#{url} " \
231
+ '(reachability not probed locally; verified end-to-end on next CI run)'
232
+ end
233
+
234
+ # Surface the narrow-attribution precondition at diagnostic time.
235
+ # When `track_ar_schema_notifications` is enabled AND Rails is
236
+ # loaded AND the rspec-rails default `use_transactional_fixtures
237
+ # = true` is in effect, per-example BEGIN/COMMIT fires
238
+ # `sql.active_record` inside the rspec-tracer bucket and
239
+ # attribution silently widens. Same shape as the boot-time warn
240
+ # in RSpecTracer.start, surfaced here so users running
241
+ # `rspec-tracer doctor` see the issue without having to boot a
242
+ # full rspec run first.
243
+ def self.ar_schema_narrow_attribution_check
244
+ return 'INFO AR schema: track_ar_schema_notifications not enabled' unless ar_schema_enabled?
245
+ return 'INFO AR schema: Rails not loaded' unless rails_loaded?
246
+
247
+ if transactional_fixtures_default?
248
+ 'WARN AR schema: track_ar_schema_notifications + use_transactional_fixtures=true ' \
249
+ 'silently widens to whole-suite-on-schema-change. See README ' \
250
+ 'section "Narrow AR schema attribution".'
251
+ else
252
+ 'OK AR schema: narrow attribution preconditions look good'
253
+ end
254
+ end
255
+
256
+ # Internal helper for the tracer pipeline.
257
+ # @api private
258
+ def self.ar_schema_enabled?
259
+ return false unless RSpecTracer.respond_to?(:track_ar_schema_notifications?)
260
+
261
+ RSpecTracer.track_ar_schema_notifications?
262
+ end
263
+
264
+ # Internal helper for the tracer pipeline.
265
+ # @api private
266
+ def self.rails_loaded?
267
+ defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
268
+ end
269
+
270
+ # Internal helper for the tracer pipeline.
271
+ # @api private
272
+ def self.transactional_fixtures_default?
273
+ return false unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)
274
+
275
+ cfg = ::RSpec.configuration
276
+ return false unless cfg.respond_to?(:use_transactional_fixtures)
277
+
278
+ cfg.use_transactional_fixtures != false
279
+ rescue StandardError
280
+ false
281
+ end
282
+ end
283
+ end
284
+ end