Module: RSpecTracer::CLI::Doctor Private

Defined in:
lib/rspec_tracer/cli/doctor.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

rspec-tracer doctor — diagnose config + environment. Reports Ruby + rspec-tracer versions, project root resolution, cache / coverage / report directory state, and SimpleCov / Rails presence. Exits 0 on healthy diagnosis, 1 if any check fails.

Constant Summary collapse

REMOTE_CACHE_PROBES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

When remote_cache is configured, verify the backend is reachable from doctor's vantage point so the user catches misconfig (typo'd S3 path / unreachable Redis URL / unwritable local-fs dir) BEFORE the next CI run fails. Best-effort: never FAIL the gate, just surface a WARN/INFO line.

{
  s3: ->(opts) { remote_cache_s3_check(opts) },
  local_fs: ->(opts) { remote_cache_local_fs_check(opts) },
  redis: ->(opts) { remote_cache_redis_check(opts) }
}.freeze
CI_ENV_VARS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Environment variables that signal a CI environment, one per major provider plus the near-universal CI convention.

%w[CI GITHUB_ACTIONS GITLAB_CI CIRCLECI BUILDKITE TF_BUILD].freeze

Class Method Summary collapse

Class Method Details

.ar_schema_enabled?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.

Returns:

  • (Boolean)


281
282
283
284
285
# File 'lib/rspec_tracer/cli/doctor.rb', line 281

def self.ar_schema_enabled?
  return false unless RSpecTracer.respond_to?(:track_ar_schema_notifications?)

  RSpecTracer.track_ar_schema_notifications?
end

.ar_schema_narrow_attribution_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Surface the narrow-attribution precondition at diagnostic time. When track_ar_schema_notifications is enabled AND Rails is loaded AND the rspec-rails default `use_transactional_fixtures

true` is in effect, per-example BEGIN/COMMIT fires

sql.active_record inside the rspec-tracer bucket and attribution silently widens. Same shape as the boot-time warn in RSpecTracer.start, surfaced here so users running rspec-tracer doctor see the issue without having to boot a full rspec run first.



246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/rspec_tracer/cli/doctor.rb', line 246

def self.ar_schema_narrow_attribution_check
  return 'INFO AR schema:   track_ar_schema_notifications not enabled' unless ar_schema_enabled?
  return 'INFO AR schema:   Rails not loaded' unless rails_loaded?

  if transactional_fixtures_default?
    'WARN AR schema:   track_ar_schema_notifications + use_transactional_fixtures=true ' \
      'silently widens to whole-suite-on-schema-change. See README ' \
      'section "Narrow AR schema attribution".'
  else
    'OK   AR schema:   narrow attribution preconditions look good'
  end
end

.cache_path_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



79
80
81
# File 'lib/rspec_tracer/cli/doctor.rb', line 79

def self.cache_path_check
  path_check('cache_path:', RSpecTracer.cache_path)
end

.cache_schema_version_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Surface a 1.x->2.0 cache mismatch BEFORE the user runs rspec and watches everything re-run mysteriously. Reads the cached last_run.json (if any) and compares its schema_version against the gem's Schema::CURRENT. Three outcomes: no cache yet (INFO), match (OK), or mismatch (WARN with the cold-run note). Never FAIL - schema mismatches are the documented cold-run path, not a hard error.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/rspec_tracer/cli/doctor.rb', line 151

def self.cache_schema_version_check
  require 'rspec_tracer/storage/schema'
  require 'json'

  cache_path = RSpecTracer.cache_path
  last_run_path = File.join(cache_path.to_s, 'last_run.json')
  unless File.file?(last_run_path)
    return 'INFO schema:      no cache yet (next rspec run is cold; expected on first install)'
  end

  manifest = JSON.parse(File.read(last_run_path, encoding: 'UTF-8'))
  stored = manifest['schema_version']
  current = RSpecTracer::Storage::Schema::CURRENT
  if stored == current
    "OK   schema:      v#{current} (matches gem)"
  else
    "WARN schema:      stored v#{stored.inspect} != gem v#{current} " \
      '(next rspec run is a cold run; expected on 1.x->2.0 upgrade)'
  end
rescue StandardError => e
  "WARN schema:      could not read cache manifest: #{e.class}: #{e.message}"
end

.ci_environment_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Surface the cache-persistence recipes exactly when the user is on CI, where a non-persisted cache silently degrades every run to a cold run. Detection requires a var that is set AND non-empty: ENV['CI'] == "" is truthy in Ruby (a shell with CI= exported would otherwise read as detected). INFO both ways - being in or out of CI is not a health state, so this check never affects the exit status.



270
271
272
273
274
275
276
277
# File 'lib/rspec_tracer/cli/doctor.rb', line 270

def self.ci_environment_check
  var = CI_ENV_VARS.find { |v| !ENV[v].to_s.empty? }
  if var
    "INFO ci:          detected via ENV[#{var}] -- cache persistence recipes: docs/CI_RECIPES.md"
  else
    'INFO ci:          not detected (local run)'
  end
end

.coverage_path_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



85
86
87
# File 'lib/rspec_tracer/cli/doctor.rb', line 85

def self.coverage_path_check
  path_check('coverage_path:', RSpecTracer.coverage_path)
end

.git_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



107
108
109
110
111
112
113
# File 'lib/rspec_tracer/cli/doctor.rb', line 107

def self.git_check
  if system('git', 'rev-parse', 'HEAD', out: File::NULL, err: File::NULL)
    'OK   git:         HEAD reachable (remote_cache will work)'
  else
    'WARN git:         not in a git repo (remote_cache will degrade gracefully)'
  end
end

.path_check(label, path) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



97
98
99
100
101
102
103
# File 'lib/rspec_tracer/cli/doctor.rb', line 97

def self.path_check(label, path)
  return "FAIL #{label} <missing>" if path.nil? || path.empty?
  return "FAIL #{label} #{path} (does not exist)" unless File.directory?(path)
  return "FAIL #{label} #{path} (not writable)" unless File.writable?(path)

  "OK   #{label} #{path}"
end

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



49
50
51
52
53
54
55
56
57
# File 'lib/rspec_tracer/cli/doctor.rb', line 49

def self.print_help(stdout)
  stdout.puts <<~HELP
    Usage: rspec-tracer doctor

    Diagnose rspec-tracer config and environment. Prints a checklist
    of versions, paths, and integrations; exits 0 if all checks pass.
  HELP
  0
end

.project_root_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



73
74
75
# File 'lib/rspec_tracer/cli/doctor.rb', line 73

def self.project_root_check
  "OK   root:        #{RSpecTracer.root}"
end

.rails_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

See simplecov_check for the doctor-runs-in-its-own- process rationale. Same three-state probe shape: loaded in this process / installed but not loaded / not installed.



135
136
137
138
139
140
141
142
# File 'lib/rspec_tracer/cli/doctor.rb', line 135

def self.rails_check
  return "OK   Rails:       #{::Rails::VERSION::STRING}" if defined?(::Rails::VERSION) && !::Rails::VERSION.nil?

  spec = Gem.loaded_specs['rails']
  return "INFO Rails:       installed (v#{spec.version}; not loaded in doctor's process)" if spec

  'INFO Rails:       not installed (this is fine for non-Rails projects)'
end

.rails_loaded?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.

Returns:

  • (Boolean)


289
290
291
# File 'lib/rspec_tracer/cli/doctor.rb', line 289

def self.rails_loaded?
  defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
end

.remote_cache_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



187
188
189
190
191
192
193
194
195
196
# File 'lib/rspec_tracer/cli/doctor.rb', line 187

def self.remote_cache_check
  entry = remote_cache_entry
  return 'INFO remote_cache: not configured (skip)' unless entry

  backend, opts = entry
  probe = REMOTE_CACHE_PROBES[backend]
  return "INFO remote_cache: custom backend #{backend.inspect} (skipping reachability probe)" if probe.nil?

  instance_exec(opts, &probe)
end

.remote_cache_entryObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



200
201
202
203
204
# File 'lib/rspec_tracer/cli/doctor.rb', line 200

def self.remote_cache_entry
  return nil unless RSpecTracer.respond_to?(:remote_cache_backend_entry)

  RSpecTracer.remote_cache_backend_entry
end

.remote_cache_local_fs_check(opts) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



218
219
220
221
222
223
224
225
# File 'lib/rspec_tracer/cli/doctor.rb', line 218

def self.remote_cache_local_fs_check(opts)
  path = opts[:path] || opts['path']
  return 'WARN remote_cache: :local_fs configured without :path' if path.nil? || path.empty?
  return "WARN remote_cache: :local_fs path #{path} does not exist" unless File.directory?(path)
  return "WARN remote_cache: :local_fs path #{path} not writable" unless File.writable?(path)

  "OK   remote_cache: :local_fs path=#{path}"
end

.remote_cache_redis_check(opts) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



229
230
231
232
233
234
235
# File 'lib/rspec_tracer/cli/doctor.rb', line 229

def self.remote_cache_redis_check(opts)
  url = opts[:url] || opts['url'] || ENV.fetch('RSPEC_TRACER_REMOTE_CACHE_URI', nil)
  return 'WARN remote_cache: :redis configured without :url' if url.nil? || url.empty?

  "OK   remote_cache: :redis url=#{url} " \
    '(reachability not probed locally; verified end-to-end on next CI run)'
end

.remote_cache_s3_check(opts) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



208
209
210
211
212
213
214
# File 'lib/rspec_tracer/cli/doctor.rb', line 208

def self.remote_cache_s3_check(opts)
  bucket = opts[:bucket] || opts['bucket']
  return 'WARN remote_cache: :s3 configured without :bucket' if bucket.nil? || bucket.empty?

  "OK   remote_cache: :s3 bucket=#{bucket} " \
    '(reachability not probed locally; verified end-to-end on next CI run)'
end

.report_path_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



91
92
93
# File 'lib/rspec_tracer/cli/doctor.rb', line 91

def self.report_path_check
  path_check('report_path:', RSpecTracer.report_path)
end

.ruby_version_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



61
62
63
# File 'lib/rspec_tracer/cli/doctor.rb', line 61

def self.ruby_version_check
  "OK   ruby:        #{RUBY_DESCRIPTION}"
end

.run(args, stdout: $stdout, stderr: $stderr) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns exit status (0 = healthy, 1 = any check FAILed; warnings keep status 0).

Parameters:

  • args (Array<String>)

    sub-command args (-h / --help).

  • stdout (IO) (defaults to: $stdout)
  • stderr (IO) (defaults to: $stderr)

Returns:

  • (Integer)

    exit status (0 = healthy, 1 = any check FAILed; warnings keep status 0).



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rspec_tracer/cli/doctor.rb', line 17

def self.run(args, stdout: $stdout, stderr: $stderr)
  return print_help(stdout) if args.include?('-h') || args.include?('--help')

  checks = [
    ruby_version_check,
    tracer_version_check,
    project_root_check,
    cache_path_check,
    coverage_path_check,
    report_path_check,
    git_check,
    simplecov_check,
    rails_check,
    cache_schema_version_check,
    remote_cache_check,
    ar_schema_narrow_attribution_check,
    ci_environment_check
  ]
  checks.each { |line| stdout.puts line }
  ok = checks.none? { |line| line.start_with?('FAIL') }
  ok ? 0 : 1
rescue Errno::EPIPE
  # Downstream pipe (`... | head`) closed early -- routine in
  # shell pipelines, not a failure. Exit 0 silently.
  0
rescue StandardError => e
  stderr.puts "doctor: #{e.class}: #{e.message}"
  1
end

.simplecov_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

bundle exec rspec-tracer doctor runs in its own process via the gem's bin/rspec-tracer binstub, NOT inside the user's rspec boot — so app code never loads here and a bare defined?(::SimpleCov) check would falsely report "not loaded" on projects that DO have SimpleCov in their Gemfile. Probe Gem.loaded_specs first to surface the "installed but not loaded in doctor's process" case separately from "actually not installed."



123
124
125
126
127
128
129
130
# File 'lib/rspec_tracer/cli/doctor.rb', line 123

def self.simplecov_check
  return 'OK   SimpleCov:   loaded (interop active)' if defined?(::SimpleCov)

  spec = Gem.loaded_specs['simplecov']
  return "INFO SimpleCov:   installed (v#{spec.version}; not loaded in doctor's process)" if spec

  'INFO SimpleCov:   not installed (this is fine; SimpleCov is optional)'
end

.tracer_version_checkObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.



67
68
69
# File 'lib/rspec_tracer/cli/doctor.rb', line 67

def self.tracer_version_check
  "OK   rspec-tracer: #{RSpecTracer::VERSION}"
end

.transactional_fixtures_default?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal helper for the tracer pipeline.

Returns:

  • (Boolean)


295
296
297
298
299
300
301
302
303
304
# File 'lib/rspec_tracer/cli/doctor.rb', line 295

def self.transactional_fixtures_default?
  return false unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)

  cfg = ::RSpec.configuration
  return false unless cfg.respond_to?(:use_transactional_fixtures)

  cfg.use_transactional_fixtures != false
rescue StandardError
  false
end