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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +384 -67
- data/README.md +454 -429
- data/bin/rspec-tracer +15 -0
- data/lib/rspec_tracer/cache/Rakefile +43 -0
- data/lib/rspec_tracer/cli/cache_clear.rb +111 -0
- data/lib/rspec_tracer/cli/cache_info.rb +104 -0
- data/lib/rspec_tracer/cli/doctor.rb +284 -0
- data/lib/rspec_tracer/cli/explain.rb +158 -0
- data/lib/rspec_tracer/cli/report_open.rb +82 -0
- data/lib/rspec_tracer/cli.rb +116 -0
- data/lib/rspec_tracer/configuration.rb +1196 -3
- data/lib/rspec_tracer/engine.rb +1168 -0
- data/lib/rspec_tracer/example.rb +141 -11
- data/lib/rspec_tracer/filter.rb +35 -0
- data/lib/rspec_tracer/line_stub.rb +61 -0
- data/lib/rspec_tracer/load_config.rb +2 -2
- data/lib/rspec_tracer/logger.rb +15 -0
- data/lib/rspec_tracer/rails/README.md +78 -0
- data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
- data/lib/rspec_tracer/rails/notifications.rb +263 -0
- data/lib/rspec_tracer/rails/preset.rb +94 -0
- data/lib/rspec_tracer/rails/railtie.rb +22 -0
- data/lib/rspec_tracer/rails.rb +15 -0
- data/lib/rspec_tracer/remote_cache/README.md +140 -0
- data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
- data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
- data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
- data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
- data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
- data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
- data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
- data/lib/rspec_tracer/remote_cache/user_tasks.rb +436 -0
- data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
- data/lib/rspec_tracer/remote_cache.rb +22 -0
- data/lib/rspec_tracer/reporters/README.md +103 -0
- data/lib/rspec_tracer/reporters/base.rb +87 -0
- data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
- data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
- data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
- data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
- data/lib/rspec_tracer/reporters/html/README.md +80 -0
- data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
- data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
- data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
- data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
- data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
- data/lib/rspec_tracer/reporters/html/package.json +29 -0
- data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
- data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
- data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
- data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
- data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
- data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
- data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
- data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
- data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
- data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
- data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
- data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
- data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
- data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
- data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
- data/lib/rspec_tracer/reporters/registry.rb +120 -0
- data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
- data/lib/rspec_tracer/rspec/README.md +73 -0
- data/lib/rspec_tracer/rspec/installation.rb +97 -0
- data/lib/rspec_tracer/rspec/metadata.rb +96 -0
- data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
- data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
- data/lib/rspec_tracer/rspec/runner_hook.rb +239 -0
- data/lib/rspec_tracer/source_file.rb +24 -7
- data/lib/rspec_tracer/storage/README.md +35 -0
- data/lib/rspec_tracer/storage/backend.rb +130 -0
- data/lib/rspec_tracer/storage/json_backend.rb +884 -0
- data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
- data/lib/rspec_tracer/storage/schema.rb +50 -0
- data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
- data/lib/rspec_tracer/storage/serializer/msgpack.rb +167 -0
- data/lib/rspec_tracer/storage/snapshot.rb +141 -0
- data/lib/rspec_tracer/storage/sqlite_backend.rb +693 -0
- data/lib/rspec_tracer/time_formatter.rb +37 -18
- data/lib/rspec_tracer/tracker/README.md +36 -0
- data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
- data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
- data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
- data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
- data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
- data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
- data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
- data/lib/rspec_tracer/tracker/filter.rb +127 -0
- data/lib/rspec_tracer/tracker/input.rb +99 -0
- data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
- data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
- data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
- data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
- data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
- data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
- data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
- data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
- data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
- data/lib/rspec_tracer/version.rb +4 -1
- data/lib/rspec_tracer.rb +231 -491
- metadata +94 -43
- data/lib/rspec_tracer/cache.rb +0 -207
- data/lib/rspec_tracer/coverage_merger.rb +0 -42
- data/lib/rspec_tracer/coverage_reporter.rb +0 -187
- data/lib/rspec_tracer/coverage_writer.rb +0 -58
- data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
- data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
- data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
- data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
- data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
- data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
- data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
- data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
- data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
- data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
- data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
- data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
- data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
- data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
- data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
- data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
- data/lib/rspec_tracer/report_generator.rb +0 -158
- data/lib/rspec_tracer/report_merger.rb +0 -68
- data/lib/rspec_tracer/report_writer.rb +0 -141
- data/lib/rspec_tracer/reporter.rb +0 -204
- data/lib/rspec_tracer/rspec_reporter.rb +0 -41
- data/lib/rspec_tracer/rspec_runner.rb +0 -56
- data/lib/rspec_tracer/ruby_coverage.rb +0 -9
- data/lib/rspec_tracer/runner.rb +0 -278
|
@@ -0,0 +1,158 @@
|
|
|
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 explain <example>` — show why a given example is
|
|
13
|
+
# scheduled to run or skip on the next rspec invocation. Backend-
|
|
14
|
+
# agnostic: dispatches through {RSpecTracer::Storage::Backend.build}
|
|
15
|
+
# so `storage_backend :sqlite` resolves the latest run from the
|
|
16
|
+
# meta table instead of the JsonBackend-only `last_run.json` file.
|
|
17
|
+
module Explain
|
|
18
|
+
# @param args [Array<String>] sub-command args. First positional
|
|
19
|
+
# arg is the example_id (or substring) to explain.
|
|
20
|
+
# @param stdout [IO]
|
|
21
|
+
# @param stderr [IO]
|
|
22
|
+
# @return [Integer] exit status (0 = explanation printed,
|
|
23
|
+
# 1 = example not found / cache missing).
|
|
24
|
+
def self.run(args, stdout: $stdout, stderr: $stderr)
|
|
25
|
+
return print_help(stdout) if args.empty? || args.include?('-h') || args.include?('--help')
|
|
26
|
+
|
|
27
|
+
require 'rspec_tracer/load_config'
|
|
28
|
+
cache_path = RSpecTracer.cache_path
|
|
29
|
+
|
|
30
|
+
snapshot = load_snapshot(cache_path, stderr)
|
|
31
|
+
return 1 if snapshot.nil?
|
|
32
|
+
|
|
33
|
+
match = find_example(snapshot.all_examples, args.first)
|
|
34
|
+
return no_match(args.first, snapshot.all_examples, stderr) if match.nil?
|
|
35
|
+
|
|
36
|
+
print_explanation(stdout, match, snapshot)
|
|
37
|
+
0
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
stderr.puts "explain: #{e.class}: #{e.message}"
|
|
40
|
+
1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Internal helper for the tracer pipeline.
|
|
44
|
+
# @api private
|
|
45
|
+
def self.load_snapshot(cache_path, stderr)
|
|
46
|
+
backend = Storage::Backend.build(cache_path: cache_path, configuration: RSpecTracer)
|
|
47
|
+
run_id = backend.last_run_id
|
|
48
|
+
if run_id.nil? || run_id.to_s.empty?
|
|
49
|
+
stderr.puts "explain: no cache yet at #{cache_path} — run rspec first"
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
snapshot = backend.load_graph(schema_version: Storage::Schema::CURRENT)
|
|
54
|
+
if snapshot.nil?
|
|
55
|
+
stderr.puts "explain: cache at #{cache_path} is incompatible with this rspec-tracer; next rspec run is cold"
|
|
56
|
+
return nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
snapshot
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Internal helper for the tracer pipeline.
|
|
63
|
+
# @api private
|
|
64
|
+
def self.no_match(query, all_examples, stderr)
|
|
65
|
+
stderr.puts "explain: no example matching #{query.inspect}"
|
|
66
|
+
stderr.puts " cache has #{all_examples.size} examples; pass an example_id or substring of description"
|
|
67
|
+
1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Internal helper for the tracer pipeline.
|
|
71
|
+
# @api private
|
|
72
|
+
def self.print_help(stdout)
|
|
73
|
+
stdout.puts <<~HELP
|
|
74
|
+
Usage: rspec-tracer explain <example_id_or_substring>
|
|
75
|
+
|
|
76
|
+
Show why an example is scheduled to run or skip. Matches against
|
|
77
|
+
example_id exactly first, then falls back to a substring match
|
|
78
|
+
on the example's full_description. Backend-aware: works under
|
|
79
|
+
`storage_backend :json` (default) and `storage_backend :sqlite`.
|
|
80
|
+
Requires a prior rspec run.
|
|
81
|
+
HELP
|
|
82
|
+
0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Internal helper for the tracer pipeline.
|
|
86
|
+
# @api private
|
|
87
|
+
def self.find_example(all_examples, query)
|
|
88
|
+
return all_examples[query] if all_examples.key?(query)
|
|
89
|
+
|
|
90
|
+
all_examples.find do |id, meta|
|
|
91
|
+
meta = {} unless meta.is_a?(::Hash)
|
|
92
|
+
desc = fetch_meta(meta, 'full_description') || fetch_meta(meta, 'description') || ''
|
|
93
|
+
id.include?(query) || desc.include?(query)
|
|
94
|
+
end&.last
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Internal helper for the tracer pipeline.
|
|
98
|
+
# @api private
|
|
99
|
+
def self.print_explanation(stdout, meta, snapshot)
|
|
100
|
+
meta = {} unless meta.is_a?(::Hash)
|
|
101
|
+
format_lines(meta).each { |line| stdout.puts line }
|
|
102
|
+
print_dependency_summary(stdout, meta, snapshot)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Internal helper for the tracer pipeline.
|
|
106
|
+
# @api private
|
|
107
|
+
def self.format_lines(meta)
|
|
108
|
+
id = fetch_meta(meta, 'example_id', 'id') || '<unknown>'
|
|
109
|
+
file = fetch_meta(meta, 'rerun_file_name', 'file_name')
|
|
110
|
+
line = fetch_meta(meta, 'rerun_line_number', 'line_number')
|
|
111
|
+
status = dig_meta(meta, 'execution_result', 'status') || fetch_meta(meta, 'status') || 'unknown'
|
|
112
|
+
[
|
|
113
|
+
"id: #{id}",
|
|
114
|
+
"description: #{fetch_meta(meta, 'full_description', 'description')}",
|
|
115
|
+
"location: #{file}:#{line}",
|
|
116
|
+
"last status: #{status}",
|
|
117
|
+
"run reason: #{fetch_meta(meta, 'run_reason') || '<not recorded>'}"
|
|
118
|
+
]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Look up a key from a Hash, tolerating both String and Symbol
|
|
122
|
+
# storage. Snapshot Hashes round-tripped through JSON yield
|
|
123
|
+
# String keys; the post-#182 msgpack serializer preserves
|
|
124
|
+
# Symbol keys end-to-end, so callers can't assume either shape.
|
|
125
|
+
def self.fetch_meta(meta, *keys)
|
|
126
|
+
keys.each do |k|
|
|
127
|
+
v = meta[k]
|
|
128
|
+
return v unless v.nil?
|
|
129
|
+
|
|
130
|
+
sym_value = meta[k.to_sym]
|
|
131
|
+
return sym_value unless sym_value.nil?
|
|
132
|
+
end
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Look up a nested key from a Hash, tolerating both String and
|
|
137
|
+
# Symbol storage at each level. See {.fetch_meta} for rationale.
|
|
138
|
+
def self.dig_meta(meta, *keys)
|
|
139
|
+
keys.reduce(meta) do |acc, k|
|
|
140
|
+
break nil if acc.nil? || !acc.is_a?(::Hash)
|
|
141
|
+
|
|
142
|
+
acc[k] || acc[k.to_sym]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Internal helper for the tracer pipeline.
|
|
147
|
+
# @api private
|
|
148
|
+
def self.print_dependency_summary(stdout, meta, snapshot)
|
|
149
|
+
id = fetch_meta(meta, 'example_id', 'id')
|
|
150
|
+
deps = snapshot.dependency || {}
|
|
151
|
+
files = Array(deps[id])
|
|
152
|
+
stdout.puts "dependencies: #{files.size} files tracked"
|
|
153
|
+
files.first(10).each { |f| stdout.puts " - #{f}" }
|
|
154
|
+
stdout.puts " ... (#{files.size - 10} more)" if files.size > 10
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
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 report:open` — open the HTML report in the default
|
|
8
|
+
# browser. Resolves `report_path/index.html` and dispatches via
|
|
9
|
+
# `open` (macOS) / `xdg-open` (Linux). Falls back to printing the
|
|
10
|
+
# path when no opener is available.
|
|
11
|
+
module ReportOpen
|
|
12
|
+
# @param args [Array<String>] sub-command args (`-h` / `--help`).
|
|
13
|
+
# @param stdout [IO]
|
|
14
|
+
# @param stderr [IO]
|
|
15
|
+
# @return [Integer] exit status (0 = opened or path printed,
|
|
16
|
+
# 1 = report missing).
|
|
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
|
+
report_path = RSpecTracer.report_path
|
|
23
|
+
index_path = File.join(report_path, 'index.html')
|
|
24
|
+
|
|
25
|
+
unless File.file?(index_path)
|
|
26
|
+
stderr.puts "report:open: no report at #{index_path}"
|
|
27
|
+
stderr.puts ' run rspec first to generate the HTML report'
|
|
28
|
+
return 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
opener = detect_opener
|
|
32
|
+
if opener.nil?
|
|
33
|
+
stdout.puts "report:open: report at #{index_path}"
|
|
34
|
+
stdout.puts ' no opener detected (open / xdg-open); open the path manually'
|
|
35
|
+
return 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if system(opener, index_path, out: File::NULL, err: File::NULL)
|
|
39
|
+
stdout.puts "report:open: opened #{index_path} via #{opener}"
|
|
40
|
+
0
|
|
41
|
+
else
|
|
42
|
+
stderr.puts "report:open: failed to launch #{opener} #{index_path}"
|
|
43
|
+
1
|
|
44
|
+
end
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
stderr.puts "report:open: #{e.class}: #{e.message}"
|
|
47
|
+
1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Internal helper for the tracer pipeline.
|
|
51
|
+
# @api private
|
|
52
|
+
def self.print_help(stdout)
|
|
53
|
+
stdout.puts <<~HELP
|
|
54
|
+
Usage: rspec-tracer report:open
|
|
55
|
+
|
|
56
|
+
Open the HTML report (`report_path/index.html`) in the default
|
|
57
|
+
browser. Uses `open` on macOS and `xdg-open` on Linux. Prints
|
|
58
|
+
the path and exits 0 when no opener is available.
|
|
59
|
+
HELP
|
|
60
|
+
0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the opener binary name on PATH, or nil. Checked in
|
|
64
|
+
# priority order: macOS `open`, then Linux `xdg-open`. Windows
|
|
65
|
+
# is unsupported per `COMPATIBILITY_MATRIX.md`'s explicit drop;
|
|
66
|
+
# users on Windows see the print-the-path fallback.
|
|
67
|
+
def self.detect_opener
|
|
68
|
+
%w[open xdg-open].find { |bin| which(bin) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Internal helper for the tracer pipeline.
|
|
72
|
+
# @api private
|
|
73
|
+
def self.which(binary)
|
|
74
|
+
found = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
75
|
+
path = File.join(dir, binary)
|
|
76
|
+
File.file?(path) && File.executable?(path)
|
|
77
|
+
end
|
|
78
|
+
found ? binary : nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
# Require the top-level entry to pull in `docile` (used by
|
|
6
|
+
# `Configuration#configure`) plus the autoloads sub-commands rely on.
|
|
7
|
+
# Loading `rspec_tracer` does NOT call `RSpecTracer.start` - the engine
|
|
8
|
+
# stays inert until a user invokes it explicitly.
|
|
9
|
+
require 'rspec_tracer'
|
|
10
|
+
|
|
11
|
+
module RSpecTracer
|
|
12
|
+
# Command-line entry for the `rspec-tracer` binary. Five sub-commands
|
|
13
|
+
# per USER_FACING_SURFACE.md §10: `doctor`, `cache:info`, `cache:clear`,
|
|
14
|
+
# `report:open`, `explain <example>`.
|
|
15
|
+
#
|
|
16
|
+
# The CLI is opt-in — the canonical CI flow continues to go through
|
|
17
|
+
# `rake rspec_tracer:remote_cache:*` tasks per USER_FACING_SURFACE.md
|
|
18
|
+
# §5. Sub-commands operate against the local cache + report
|
|
19
|
+
# directories resolved from the project's `.rspec-tracer` config
|
|
20
|
+
# (loaded lazily on first sub-command dispatch, not at CLI load time —
|
|
21
|
+
# `doctor` deliberately runs without requiring a configured project).
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
module CLI
|
|
25
|
+
# Internal constant.
|
|
26
|
+
# @api private
|
|
27
|
+
SUB_COMMANDS = {
|
|
28
|
+
'doctor' => 'Doctor',
|
|
29
|
+
'cache:info' => 'CacheInfo',
|
|
30
|
+
'cache:clear' => 'CacheClear',
|
|
31
|
+
'report:open' => 'ReportOpen',
|
|
32
|
+
'explain' => 'Explain'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# CLI entry. Called by `bin/rspec-tracer` with `ARGV`. Wraps every
|
|
36
|
+
# sub-command in a top-level rescue so the binary always exits
|
|
37
|
+
# with a meaningful integer status (0 / 1) instead of a backtrace.
|
|
38
|
+
#
|
|
39
|
+
# @param argv [Array<String>] command-line arguments (excluding
|
|
40
|
+
# the program name)
|
|
41
|
+
# @param stdout [IO] stream for normal output (default `$stdout`)
|
|
42
|
+
# @param stderr [IO] stream for errors / diagnostics (default `$stderr`)
|
|
43
|
+
# @return [Integer] exit status (0 = success, 1 = failure)
|
|
44
|
+
def self.run(argv, stdout: $stdout, stderr: $stderr)
|
|
45
|
+
args = argv.dup
|
|
46
|
+
return print_top_level_help(stdout) if args.empty? || %w[-h --help help].include?(args.first)
|
|
47
|
+
return print_version(stdout) if %w[-v --version].include?(args.first)
|
|
48
|
+
|
|
49
|
+
dispatch(args, stdout: stdout, stderr: stderr)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
stderr.puts "rspec-tracer: #{e.class}: #{e.message}"
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Internal helper for the tracer pipeline.
|
|
56
|
+
# @api private
|
|
57
|
+
def self.dispatch(args, stdout:, stderr:)
|
|
58
|
+
sub = args.shift
|
|
59
|
+
klass_name = SUB_COMMANDS[sub]
|
|
60
|
+
return unknown_sub_command(sub, stderr) if klass_name.nil?
|
|
61
|
+
|
|
62
|
+
load_sub_command(klass_name)
|
|
63
|
+
RSpecTracer::CLI.const_get(klass_name).run(args, stdout: stdout, stderr: stderr)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Internal helper for the tracer pipeline.
|
|
67
|
+
# @api private
|
|
68
|
+
def self.unknown_sub_command(sub, stderr)
|
|
69
|
+
stderr.puts "rspec-tracer: unknown sub-command #{sub.inspect}"
|
|
70
|
+
stderr.puts " available: #{SUB_COMMANDS.keys.join(', ')}"
|
|
71
|
+
1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Internal helper for the tracer pipeline.
|
|
75
|
+
# @api private
|
|
76
|
+
def self.print_top_level_help(stdout)
|
|
77
|
+
stdout.puts <<~HELP
|
|
78
|
+
Usage: rspec-tracer <sub-command> [options]
|
|
79
|
+
|
|
80
|
+
Sub-commands:
|
|
81
|
+
doctor Diagnose rspec-tracer config + environment.
|
|
82
|
+
cache:info Show cache size, last run timestamp, and example counts.
|
|
83
|
+
cache:clear Remove cache, coverage, and report directories.
|
|
84
|
+
report:open Open the HTML report in the default browser.
|
|
85
|
+
explain <id> Show why an example is scheduled to run or skip.
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
-h, --help Print this help message.
|
|
89
|
+
-v, --version Print rspec-tracer version.
|
|
90
|
+
|
|
91
|
+
Run `rspec-tracer <sub-command> --help` for sub-command options.
|
|
92
|
+
HELP
|
|
93
|
+
0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Internal helper for the tracer pipeline.
|
|
97
|
+
# @api private
|
|
98
|
+
def self.print_version(stdout)
|
|
99
|
+
stdout.puts "rspec-tracer #{RSpecTracer::VERSION}"
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Internal helper for the tracer pipeline.
|
|
104
|
+
# @api private
|
|
105
|
+
def self.load_sub_command(klass_name)
|
|
106
|
+
filename = klass_name.gsub(/([A-Z])/) { |m| "_#{m.downcase}" }.sub(/^_/, '')
|
|
107
|
+
require_relative "cli/#{filename}"
|
|
108
|
+
rescue LoadError => e
|
|
109
|
+
# LoadError isn't a StandardError, so the outer `rescue StandardError`
|
|
110
|
+
# in `.run` wouldn't catch it. Re-raise as StandardError so the dispatch
|
|
111
|
+
# path stays uniform: any sub-command resolution failure prints
|
|
112
|
+
# `rspec-tracer: <class>: <message>` and exits 1.
|
|
113
|
+
raise StandardError, "could not load sub-command #{klass_name.inspect}: #{e.message}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|