Module: RSpecTracer::CLI Private

Defined in:
lib/rspec_tracer/cli.rb,
lib/rspec_tracer/cli/doctor.rb,
lib/rspec_tracer/cli/explain.rb,
lib/rspec_tracer/cli/cache_info.rb,
lib/rspec_tracer/cli/cache_clear.rb,
lib/rspec_tracer/cli/report_open.rb,
lib/rspec_tracer/cli/blast_radius.rb,
lib/rspec_tracer/cli/snapshot_helpers.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.

Internal CLI -- see RSpecTracer for the user-facing surface.

Defined Under Namespace

Modules: BlastRadius, CacheClear, CacheInfo, Doctor, Explain, ReportOpen, SnapshotHelpers

Constant Summary collapse

SUB_COMMANDS =

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.

Internal constant.

{
  'doctor' => 'Doctor',
  'cache:info' => 'CacheInfo',
  'cache:clear' => 'CacheClear',
  'report:open' => 'ReportOpen',
  'explain' => 'Explain',
  'blast-radius' => 'BlastRadius'
}.freeze

Class Method Summary collapse

Class Method Details

.dispatch(args, stdout:, stderr:) ⇒ 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.



155
156
157
158
159
160
161
162
# File 'lib/rspec_tracer/cli.rb', line 155

def self.dispatch(args, stdout:, stderr:)
  sub = args.shift
  klass_name = SUB_COMMANDS[sub]
  return unknown_sub_command(sub, stderr) if klass_name.nil?

  load_sub_command(klass_name)
  RSpecTracer::CLI.const_get(klass_name).run(args, stdout: stdout, stderr: stderr)
end

.load_sub_command(klass_name) ⇒ 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
215
216
217
# File 'lib/rspec_tracer/cli.rb', line 208

def self.load_sub_command(klass_name)
  filename = klass_name.gsub(/([A-Z])/) { |m| "_#{m.downcase}" }.sub(/^_/, '')
  require_relative "cli/#{filename}"
rescue LoadError => e
  # LoadError isn't a StandardError, so the outer `rescue StandardError`
  # in `.run` wouldn't catch it. Re-raise as StandardError so the dispatch
  # path stays uniform: any sub-command resolution failure prints
  # `rspec-tracer: <class>: <message>` and exits 1.
  raise StandardError, "could not load sub-command #{klass_name.inspect}: #{e.message}"
end

.load_tracer(stderr) ⇒ 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.

Boot the tracer library for sub-command dispatch. Loading rspec_tracer pulls in docile (used by Configuration#configure), the modules sub-commands rely on, AND the project's .rspec-tracer / ~/.rspec-tracer configs (arbitrary user Ruby, loaded by load_config). It does NOT call RSpecTracer.start - the engine stays inert until a user invokes it explicitly.

A raising config must not crash the binary with a backtrace: rescue ScriptError as well as StandardError because a syntax error in .rspec-tracer raises SyntaxError, which is not a StandardError and would sail past run's rescue.

Parameters:

  • stderr (IO)

Returns:

  • (Boolean)

    true when the tracer library (and the user configs it loads) booted cleanly



89
90
91
92
93
94
95
# File 'lib/rspec_tracer/cli.rb', line 89

def self.load_tracer(stderr)
  require 'rspec_tracer'
  true
rescue ScriptError, StandardError => e
  stderr.puts "rspec-tracer: could not load configuration (.rspec-tracer): #{e.class}: #{e.message}"
  false
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.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/rspec_tracer/cli.rb', line 174

def self.print_top_level_help(stdout)
  stdout.puts <<~HELP
    Usage: rspec-tracer <sub-command> [options]

    Sub-commands:
      doctor          Diagnose rspec-tracer config + environment.
      cache:info      Show cache size, last run timestamp, and example counts.
      cache:clear     Remove cache, coverage, and report directories.
      report:open     Open the HTML report in the default browser.
      explain <id>    Show why an example is scheduled to run or skip.
      blast-radius <f>  Show which examples a file change would re-run.

    Options:
      -h, --help      Print this help message.
      -v, --version   Print rspec-tracer version.

    Run `rspec-tracer <sub-command> --help` for sub-command options.
  HELP
  0
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.



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

def self.print_version(stdout)
  # Only the version constant - not the full library, whose boot
  # would `load` the project config and could raise before the
  # version ever printed.
  require 'rspec_tracer/version'
  stdout.puts "rspec-tracer #{RSpecTracer::VERSION}"
  0
end

.run(argv, 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.

CLI entry. Called by bin/rspec-tracer with ARGV. Wraps every sub-command in a top-level rescue so the binary always exits with a meaningful integer status (0 / 1) instead of a backtrace.

Parameters:

  • argv (Array<String>)

    command-line arguments (excluding the program name)

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

    stream for normal output (default $stdout)

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

    stream for errors / diagnostics (default $stderr)

Returns:

  • (Integer)

    exit status (0 = success, 1 = failure)



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/rspec_tracer/cli.rb', line 50

def self.run(argv, stdout: $stdout, stderr: $stderr)
  args = argv.dup
  return print_top_level_help(stdout) if args.empty? || %w[-h --help help].include?(args.first)
  return print_version(stdout) if %w[-v --version].include?(args.first)

  with_default_logger_out(stderr) do
    if load_tracer(stderr)
      with_logger_on(stderr) { dispatch(args, stdout: stdout, stderr: stderr) }
    else
      1
    end
  end
rescue Errno::EPIPE
  # A downstream pipe consumer (`rspec-tracer ... | head`) closed
  # early -- routine in shell pipelines, not a failure. Exit 0
  # without printing: writing to stderr could raise EPIPE again
  # when both streams share the closed pipe.
  0
rescue StandardError => e
  stderr.puts "rspec-tracer: #{e.class}: #{e.message}"
  1
end

.unknown_sub_command(sub, stderr) ⇒ 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.



166
167
168
169
170
# File 'lib/rspec_tracer/cli.rb', line 166

def self.unknown_sub_command(sub, stderr)
  stderr.puts "rspec-tracer: unknown sub-command #{sub.inspect}"
  stderr.puts "  available: #{SUB_COMMANDS.keys.join(', ')}"
  1
end

.with_default_logger_out(stderr) ⇒ 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.

Point Logger.default_out at the CLI's stderr for the duration of library boot + sub-command dispatch, restoring the previous value afterwards. This must wrap load_tracer: booting the library loads the project + global .rspec-tracer configs, and those can write through the logger BEFORE with_logger_on ever runs -- the 1.x-compat deprecation shims (reports_s3_path, use_local_aws) fire a one-time logger.warn at first use, and in a fresh CLI process "one-time" means every invocation. The logger's default destination is stdout -- the stream machine consumers parse -- so that warning would land ahead of e.g. the blast-radius --json document and break | jq. With the default rebound, every logger constructed during the window binds to stderr, including loggers recreated mid-load when a config sets log_level (which resets the memoized instance) after a deprecated DSL call. Normal rspec runs (whose users expect stdout logs) never enter this path and stay untouched.



115
116
117
118
119
120
121
# File 'lib/rspec_tracer/cli.rb', line 115

def self.with_default_logger_out(stderr)
  previous = RSpecTracer::Logger.default_out
  RSpecTracer::Logger.default_out = stderr
  yield
ensure
  RSpecTracer::Logger.default_out = previous
end

.with_logger_on(stderr) ⇒ 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.

Bind the tracer's internal logger to the CLI's stderr for the duration of sub-command dispatch, restoring the previous logger afterwards. Backend diagnostics run through RSpecTracer.logger (e.g. Storage::Backend.build's sqlite-unavailable fallback warning, JsonBackend's schema-mismatch info line). with_default_logger_out already redirects loggers constructed during the CLI window; this layer additionally rebinds a logger that was memoized into RSpecTracer.@logger BEFORE run was called (possible for in-process callers -- the spec suite, or a user embedding the CLI -- where the library booted earlier with the stdout default). Sub-commands like blast-radius --json promise exactly one JSON document on stdout, so a diagnostic line printed ahead of it breaks | jq; together the two layers keep ALL diagnostics on stderr alongside the CLI's own messages.

RSpecTracer.logger memoizes into @logger (Configuration#logger); the ivar is seeded directly because Configuration deliberately exposes no logger setter -- adding one would leak it into the user-facing configure DSL surface.



143
144
145
146
147
148
149
150
151
# File 'lib/rspec_tracer/cli.rb', line 143

def self.with_logger_on(stderr)
  previous = RSpecTracer.instance_variable_get(:@logger)
  RSpecTracer.instance_variable_set(
    :@logger, RSpecTracer::Logger.new(RSpecTracer.log_level, out: stderr)
  )
  yield
ensure
  RSpecTracer.instance_variable_set(:@logger, previous)
end