rspec-tracer 1.2.2 → 2.0.0.pre.1
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 +197 -45
- data/README.md +439 -429
- data/bin/rspec-tracer +15 -0
- data/lib/rspec_tracer/cache/Rakefile +43 -0
- data/lib/rspec_tracer/cli/cache_clear.rb +98 -0
- data/lib/rspec_tracer/cli/cache_info.rb +103 -0
- data/lib/rspec_tracer/cli/doctor.rb +275 -0
- data/lib/rspec_tracer/cli/explain.rb +148 -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 +1100 -3
- data/lib/rspec_tracer/engine.rb +1076 -0
- data/lib/rspec_tracer/example.rb +21 -6
- 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 +397 -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 +178 -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 +68 -0
- data/lib/rspec_tracer/storage/json_backend.rb +866 -0
- data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
- data/lib/rspec_tracer/storage/schema.rb +43 -0
- data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
- data/lib/rspec_tracer/storage/serializer/msgpack.rb +90 -0
- data/lib/rspec_tracer/storage/snapshot.rb +127 -0
- data/lib/rspec_tracer/storage/sqlite_backend.rb +686 -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 +232 -381
- metadata +93 -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
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,98 @@
|
|
|
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
|
+
force = args.include?('--yes') || args.include?('-y')
|
|
27
|
+
return aborted(stdout) unless force || confirm?(stdout)
|
|
28
|
+
|
|
29
|
+
remove_each(stdout, existing)
|
|
30
|
+
0
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
stderr.puts "cache:clear: #{e.class}: #{e.message}"
|
|
33
|
+
1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Internal helper for the tracer pipeline.
|
|
37
|
+
# @api private
|
|
38
|
+
def self.existing_targets
|
|
39
|
+
[RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path]
|
|
40
|
+
.select { |path| File.directory?(path) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Internal helper for the tracer pipeline.
|
|
44
|
+
# @api private
|
|
45
|
+
def self.nothing_to_remove(stdout)
|
|
46
|
+
stdout.puts 'cache:clear: nothing to remove (cache directories do not exist)'
|
|
47
|
+
0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Internal helper for the tracer pipeline.
|
|
51
|
+
# @api private
|
|
52
|
+
def self.announce(stdout, existing)
|
|
53
|
+
stdout.puts 'cache:clear: will remove:'
|
|
54
|
+
existing.each { |path| stdout.puts " - #{path}" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Internal helper for the tracer pipeline.
|
|
58
|
+
# @api private
|
|
59
|
+
def self.confirm?(stdout)
|
|
60
|
+
stdout.print 'Proceed? [y/N] '
|
|
61
|
+
stdout.flush
|
|
62
|
+
response = $stdin.gets&.chomp&.downcase
|
|
63
|
+
%w[y yes].include?(response)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Internal helper for the tracer pipeline.
|
|
67
|
+
# @api private
|
|
68
|
+
def self.aborted(stdout)
|
|
69
|
+
stdout.puts 'cache:clear: aborted'
|
|
70
|
+
0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Internal helper for the tracer pipeline.
|
|
74
|
+
# @api private
|
|
75
|
+
def self.remove_each(stdout, existing)
|
|
76
|
+
existing.each do |path|
|
|
77
|
+
FileUtils.rm_rf(path)
|
|
78
|
+
stdout.puts " removed #{path}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Internal helper for the tracer pipeline.
|
|
83
|
+
# @api private
|
|
84
|
+
def self.print_help(stdout)
|
|
85
|
+
stdout.puts <<~HELP
|
|
86
|
+
Usage: rspec-tracer cache:clear [--yes]
|
|
87
|
+
|
|
88
|
+
Remove cache, coverage, and report directories. The next rspec
|
|
89
|
+
run will be a cold run (full re-execution + cache rebuild).
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
-y, --yes Skip the confirmation prompt.
|
|
93
|
+
HELP
|
|
94
|
+
0
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RSpecTracer
|
|
6
|
+
# Internal CLI — see {RSpecTracer} for the user-facing surface.
|
|
7
|
+
# @api private
|
|
8
|
+
module CLI
|
|
9
|
+
# `rspec-tracer cache:info` — show cache size, last run, and
|
|
10
|
+
# invalidation stats. Reads `last_run.json` + the run-id'd JSON
|
|
11
|
+
# files written by Storage::JsonBackend.
|
|
12
|
+
module CacheInfo
|
|
13
|
+
# @param args [Array<String>] sub-command args (`-h` / `--help`).
|
|
14
|
+
# @param stdout [IO]
|
|
15
|
+
# @param stderr [IO]
|
|
16
|
+
# @return [Integer] exit status (0 = success).
|
|
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
|
+
cache_path = RSpecTracer.cache_path
|
|
23
|
+
stdout.puts "cache_path: #{cache_path}"
|
|
24
|
+
stdout.puts "size: #{format_bytes(directory_size(cache_path))}"
|
|
25
|
+
|
|
26
|
+
last_run_path = File.join(cache_path, 'last_run.json')
|
|
27
|
+
unless File.file?(last_run_path)
|
|
28
|
+
stdout.puts 'last_run: no last_run.json yet (run rspec first)'
|
|
29
|
+
return 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
manifest = JSON.parse(File.read(last_run_path, encoding: 'UTF-8'))
|
|
33
|
+
run_id = manifest['run_id']
|
|
34
|
+
stdout.puts "last_run: #{run_id}"
|
|
35
|
+
stdout.puts "generated: #{manifest['generated_at']}" if manifest['generated_at']
|
|
36
|
+
|
|
37
|
+
print_run_summary(stdout, cache_path, run_id) if run_id
|
|
38
|
+
0
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
stderr.puts "cache:info: #{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 cache:info
|
|
49
|
+
|
|
50
|
+
Show the on-disk cache size, the last run id, and example counts
|
|
51
|
+
for the most recent run. Reads `last_run.json` plus the run-id'd
|
|
52
|
+
JSON files; does not modify any files.
|
|
53
|
+
HELP
|
|
54
|
+
0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Internal helper for the tracer pipeline.
|
|
58
|
+
# @api private
|
|
59
|
+
def self.print_run_summary(stdout, cache_path, run_id)
|
|
60
|
+
run_dir = File.join(cache_path, run_id)
|
|
61
|
+
return unless File.directory?(run_dir)
|
|
62
|
+
|
|
63
|
+
all_examples_path = File.join(run_dir, 'all_examples.json')
|
|
64
|
+
return unless File.file?(all_examples_path)
|
|
65
|
+
|
|
66
|
+
data = JSON.parse(File.read(all_examples_path, encoding: 'UTF-8'))
|
|
67
|
+
total = data.size
|
|
68
|
+
stdout.puts "examples: #{total} tracked"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Internal helper for the tracer pipeline.
|
|
72
|
+
# @api private
|
|
73
|
+
def self.directory_size(path)
|
|
74
|
+
return 0 unless File.directory?(path)
|
|
75
|
+
|
|
76
|
+
total = 0
|
|
77
|
+
Dir.glob(File.join(path, '**', '*'), File::FNM_DOTMATCH).each do |entry|
|
|
78
|
+
next unless File.file?(entry)
|
|
79
|
+
|
|
80
|
+
total += File.size(entry)
|
|
81
|
+
rescue SystemCallError
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
total
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Internal helper for the tracer pipeline.
|
|
88
|
+
# @api private
|
|
89
|
+
def self.format_bytes(bytes)
|
|
90
|
+
return '0 B' if bytes <= 0
|
|
91
|
+
|
|
92
|
+
units = %w[B KB MB GB]
|
|
93
|
+
scale = bytes
|
|
94
|
+
unit_index = 0
|
|
95
|
+
while scale >= 1024 && unit_index < units.length - 1
|
|
96
|
+
scale /= 1024.0
|
|
97
|
+
unit_index += 1
|
|
98
|
+
end
|
|
99
|
+
format('%<scale>.1f %<unit>s', scale: scale, unit: units[unit_index])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
# Internal helper for the tracer pipeline.
|
|
113
|
+
# @api private
|
|
114
|
+
def self.simplecov_check
|
|
115
|
+
if defined?(::SimpleCov)
|
|
116
|
+
'OK SimpleCov: loaded (interop active)'
|
|
117
|
+
else
|
|
118
|
+
'INFO SimpleCov: not loaded (this is fine; SimpleCov is optional)'
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Internal helper for the tracer pipeline.
|
|
123
|
+
# @api private
|
|
124
|
+
def self.rails_check
|
|
125
|
+
if defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
|
|
126
|
+
"OK Rails: #{::Rails::VERSION::STRING}"
|
|
127
|
+
else
|
|
128
|
+
'INFO Rails: not loaded (this is fine for non-Rails projects)'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Surface a 1.x->2.0 cache mismatch BEFORE the user runs
|
|
133
|
+
# rspec and watches everything re-run mysteriously. Reads the
|
|
134
|
+
# cached `last_run.json` (if any) and compares its
|
|
135
|
+
# `schema_version` against the gem's `Schema::CURRENT`. Three
|
|
136
|
+
# outcomes: no cache yet (INFO), match (OK), or mismatch (WARN
|
|
137
|
+
# with the cold-run note). Never FAIL - schema mismatches are
|
|
138
|
+
# the documented cold-run path, not a hard error.
|
|
139
|
+
def self.cache_schema_version_check
|
|
140
|
+
require 'rspec_tracer/storage/schema'
|
|
141
|
+
require 'json'
|
|
142
|
+
|
|
143
|
+
cache_path = RSpecTracer.cache_path
|
|
144
|
+
last_run_path = File.join(cache_path.to_s, 'last_run.json')
|
|
145
|
+
unless File.file?(last_run_path)
|
|
146
|
+
return 'INFO schema: no cache yet (next rspec run is cold; expected on first install)'
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
manifest = JSON.parse(File.read(last_run_path, encoding: 'UTF-8'))
|
|
150
|
+
stored = manifest['schema_version']
|
|
151
|
+
current = RSpecTracer::Storage::Schema::CURRENT
|
|
152
|
+
if stored == current
|
|
153
|
+
"OK schema: v#{current} (matches gem)"
|
|
154
|
+
else
|
|
155
|
+
"WARN schema: stored v#{stored.inspect} != gem v#{current} " \
|
|
156
|
+
'(next rspec run is a cold run; expected on 1.x->2.0 upgrade)'
|
|
157
|
+
end
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
"WARN schema: could not read cache manifest: #{e.class}: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# When remote_cache is configured, verify the backend is
|
|
163
|
+
# reachable from doctor's vantage point so the user catches
|
|
164
|
+
# misconfig (typo'd S3 path / unreachable Redis URL / unwritable
|
|
165
|
+
# local-fs dir) BEFORE the next CI run fails. Best-effort: never
|
|
166
|
+
# FAIL the gate, just surface a WARN/INFO line.
|
|
167
|
+
REMOTE_CACHE_PROBES = {
|
|
168
|
+
s3: ->(opts) { remote_cache_s3_check(opts) },
|
|
169
|
+
local_fs: ->(opts) { remote_cache_local_fs_check(opts) },
|
|
170
|
+
redis: ->(opts) { remote_cache_redis_check(opts) }
|
|
171
|
+
}.freeze
|
|
172
|
+
|
|
173
|
+
# Internal helper for the tracer pipeline.
|
|
174
|
+
# @api private
|
|
175
|
+
def self.remote_cache_check
|
|
176
|
+
entry = remote_cache_entry
|
|
177
|
+
return 'INFO remote_cache: not configured (skip)' unless entry
|
|
178
|
+
|
|
179
|
+
backend, opts = entry
|
|
180
|
+
probe = REMOTE_CACHE_PROBES[backend]
|
|
181
|
+
return "INFO remote_cache: custom backend #{backend.inspect} (skipping reachability probe)" if probe.nil?
|
|
182
|
+
|
|
183
|
+
instance_exec(opts, &probe)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Internal helper for the tracer pipeline.
|
|
187
|
+
# @api private
|
|
188
|
+
def self.remote_cache_entry
|
|
189
|
+
return nil unless RSpecTracer.respond_to?(:remote_cache_backend_entry)
|
|
190
|
+
|
|
191
|
+
RSpecTracer.remote_cache_backend_entry
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Internal helper for the tracer pipeline.
|
|
195
|
+
# @api private
|
|
196
|
+
def self.remote_cache_s3_check(opts)
|
|
197
|
+
bucket = opts[:bucket] || opts['bucket']
|
|
198
|
+
return 'WARN remote_cache: :s3 configured without :bucket' if bucket.nil? || bucket.empty?
|
|
199
|
+
|
|
200
|
+
"OK remote_cache: :s3 bucket=#{bucket} " \
|
|
201
|
+
'(reachability not probed locally; verified end-to-end on next CI run)'
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Internal helper for the tracer pipeline.
|
|
205
|
+
# @api private
|
|
206
|
+
def self.remote_cache_local_fs_check(opts)
|
|
207
|
+
path = opts[:path] || opts['path']
|
|
208
|
+
return 'WARN remote_cache: :local_fs configured without :path' if path.nil? || path.empty?
|
|
209
|
+
return "WARN remote_cache: :local_fs path #{path} does not exist" unless File.directory?(path)
|
|
210
|
+
return "WARN remote_cache: :local_fs path #{path} not writable" unless File.writable?(path)
|
|
211
|
+
|
|
212
|
+
"OK remote_cache: :local_fs path=#{path}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Internal helper for the tracer pipeline.
|
|
216
|
+
# @api private
|
|
217
|
+
def self.remote_cache_redis_check(opts)
|
|
218
|
+
url = opts[:url] || opts['url'] || ENV.fetch('RSPEC_TRACER_REMOTE_CACHE_URI', nil)
|
|
219
|
+
return 'WARN remote_cache: :redis configured without :url' if url.nil? || url.empty?
|
|
220
|
+
|
|
221
|
+
"OK remote_cache: :redis url=#{url} " \
|
|
222
|
+
'(reachability not probed locally; verified end-to-end on next CI run)'
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Surface the narrow-attribution precondition at diagnostic time.
|
|
226
|
+
# When `track_ar_schema_notifications` is enabled AND Rails is
|
|
227
|
+
# loaded AND the rspec-rails default `use_transactional_fixtures
|
|
228
|
+
# = true` is in effect, per-example BEGIN/COMMIT fires
|
|
229
|
+
# `sql.active_record` inside the rspec-tracer bucket and
|
|
230
|
+
# attribution silently widens. Same shape as the boot-time warn
|
|
231
|
+
# in RSpecTracer.start, surfaced here so users running
|
|
232
|
+
# `rspec-tracer doctor` see the issue without having to boot a
|
|
233
|
+
# full rspec run first.
|
|
234
|
+
def self.ar_schema_narrow_attribution_check
|
|
235
|
+
return 'INFO AR schema: track_ar_schema_notifications not enabled' unless ar_schema_enabled?
|
|
236
|
+
return 'INFO AR schema: Rails not loaded' unless rails_loaded?
|
|
237
|
+
|
|
238
|
+
if transactional_fixtures_default?
|
|
239
|
+
'WARN AR schema: track_ar_schema_notifications + use_transactional_fixtures=true ' \
|
|
240
|
+
'silently widens to whole-suite-on-schema-change. See README ' \
|
|
241
|
+
'section "Narrow AR schema attribution".'
|
|
242
|
+
else
|
|
243
|
+
'OK AR schema: narrow attribution preconditions look good'
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Internal helper for the tracer pipeline.
|
|
248
|
+
# @api private
|
|
249
|
+
def self.ar_schema_enabled?
|
|
250
|
+
return false unless RSpecTracer.respond_to?(:track_ar_schema_notifications?)
|
|
251
|
+
|
|
252
|
+
RSpecTracer.track_ar_schema_notifications?
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Internal helper for the tracer pipeline.
|
|
256
|
+
# @api private
|
|
257
|
+
def self.rails_loaded?
|
|
258
|
+
defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Internal helper for the tracer pipeline.
|
|
262
|
+
# @api private
|
|
263
|
+
def self.transactional_fixtures_default?
|
|
264
|
+
return false unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)
|
|
265
|
+
|
|
266
|
+
cfg = ::RSpec.configuration
|
|
267
|
+
return false unless cfg.respond_to?(:use_transactional_fixtures)
|
|
268
|
+
|
|
269
|
+
cfg.use_transactional_fixtures != false
|
|
270
|
+
rescue StandardError
|
|
271
|
+
false
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RSpecTracer
|
|
6
|
+
# Internal CLI — see {RSpecTracer} for the user-facing surface.
|
|
7
|
+
# @api private
|
|
8
|
+
module CLI
|
|
9
|
+
# `rspec-tracer explain <example>` — show why a given example is
|
|
10
|
+
# scheduled to run or skip on the next rspec invocation. Reads the
|
|
11
|
+
# most recent run's JSON files (all_examples.json + dependency.json
|
|
12
|
+
# + failed_examples.json + flaky_examples.json) to surface the
|
|
13
|
+
# dependency set, last-run status, and the run-decision reason.
|
|
14
|
+
module Explain
|
|
15
|
+
# @param args [Array<String>] sub-command args. First positional
|
|
16
|
+
# arg is the example_id (or substring) to explain.
|
|
17
|
+
# @param stdout [IO]
|
|
18
|
+
# @param stderr [IO]
|
|
19
|
+
# @return [Integer] exit status (0 = explanation printed,
|
|
20
|
+
# 1 = example not found / cache missing).
|
|
21
|
+
def self.run(args, stdout: $stdout, stderr: $stderr)
|
|
22
|
+
return print_help(stdout) if args.empty? || args.include?('-h') || args.include?('--help')
|
|
23
|
+
|
|
24
|
+
require 'rspec_tracer/load_config'
|
|
25
|
+
cache_path = RSpecTracer.cache_path
|
|
26
|
+
|
|
27
|
+
run_dir = resolve_run_dir(cache_path, stderr)
|
|
28
|
+
return 1 if run_dir.nil?
|
|
29
|
+
|
|
30
|
+
all_examples = read_json(File.join(run_dir, 'all_examples.json'))
|
|
31
|
+
match = find_example(all_examples, args.first)
|
|
32
|
+
return no_match(args.first, all_examples, stderr) if match.nil?
|
|
33
|
+
|
|
34
|
+
print_explanation(stdout, match, run_dir)
|
|
35
|
+
0
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
stderr.puts "explain: #{e.class}: #{e.message}"
|
|
38
|
+
1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Internal helper for the tracer pipeline.
|
|
42
|
+
# @api private
|
|
43
|
+
def self.resolve_run_dir(cache_path, stderr)
|
|
44
|
+
last_run_path = File.join(cache_path, 'last_run.json')
|
|
45
|
+
unless File.file?(last_run_path)
|
|
46
|
+
stderr.puts "explain: no last_run.json at #{cache_path} — run rspec first"
|
|
47
|
+
return nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
run_id = JSON.parse(File.read(last_run_path, encoding: 'UTF-8'))['run_id']
|
|
51
|
+
run_dir = File.join(cache_path, run_id.to_s)
|
|
52
|
+
unless File.directory?(run_dir)
|
|
53
|
+
stderr.puts "explain: run_id #{run_id} directory missing at #{run_dir}"
|
|
54
|
+
return nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
run_dir
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Internal helper for the tracer pipeline.
|
|
61
|
+
# @api private
|
|
62
|
+
def self.no_match(query, all_examples, stderr)
|
|
63
|
+
stderr.puts "explain: no example matching #{query.inspect}"
|
|
64
|
+
stderr.puts " cache has #{all_examples.size} examples; pass an example_id or substring of description"
|
|
65
|
+
1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Internal helper for the tracer pipeline.
|
|
69
|
+
# @api private
|
|
70
|
+
def self.print_help(stdout)
|
|
71
|
+
stdout.puts <<~HELP
|
|
72
|
+
Usage: rspec-tracer explain <example_id_or_substring>
|
|
73
|
+
|
|
74
|
+
Show why an example is scheduled to run or skip. Matches against
|
|
75
|
+
example_id exactly first, then falls back to a substring match
|
|
76
|
+
on the example's full_description. Requires a prior rspec run.
|
|
77
|
+
HELP
|
|
78
|
+
0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Internal helper for the tracer pipeline.
|
|
82
|
+
# @api private
|
|
83
|
+
def self.read_json(path)
|
|
84
|
+
return {} unless File.file?(path)
|
|
85
|
+
|
|
86
|
+
parsed = JSON.parse(File.read(path, encoding: 'UTF-8'))
|
|
87
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Internal helper for the tracer pipeline.
|
|
91
|
+
# @api private
|
|
92
|
+
def self.find_example(all_examples, query)
|
|
93
|
+
return all_examples[query] if all_examples.key?(query)
|
|
94
|
+
|
|
95
|
+
all_examples.find do |id, meta|
|
|
96
|
+
meta = {} unless meta.is_a?(::Hash)
|
|
97
|
+
desc = meta['full_description'] || meta['description'] || ''
|
|
98
|
+
id.include?(query) || desc.include?(query)
|
|
99
|
+
end&.last
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Internal helper for the tracer pipeline.
|
|
103
|
+
# @api private
|
|
104
|
+
def self.print_explanation(stdout, meta, run_dir)
|
|
105
|
+
meta = {} unless meta.is_a?(::Hash)
|
|
106
|
+
format_lines(meta).each { |line| stdout.puts line }
|
|
107
|
+
print_dependency_summary(stdout, meta, run_dir)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Internal helper for the tracer pipeline.
|
|
111
|
+
# @api private
|
|
112
|
+
def self.format_lines(meta)
|
|
113
|
+
id = first_non_nil(meta, 'example_id', 'id') || '<unknown>'
|
|
114
|
+
file = first_non_nil(meta, 'rerun_file_name', 'file_name')
|
|
115
|
+
line = first_non_nil(meta, 'rerun_line_number', 'line_number')
|
|
116
|
+
status = meta.dig('execution_result', 'status') || meta['status'] || 'unknown'
|
|
117
|
+
[
|
|
118
|
+
"id: #{id}",
|
|
119
|
+
"description: #{first_non_nil(meta, 'full_description', 'description')}",
|
|
120
|
+
"location: #{file}:#{line}",
|
|
121
|
+
"last status: #{status}",
|
|
122
|
+
"run reason: #{meta['run_reason'] || '<not recorded>'}"
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Internal helper for the tracer pipeline.
|
|
127
|
+
# @api private
|
|
128
|
+
def self.first_non_nil(meta, *keys)
|
|
129
|
+
keys.each { |k| return meta[k] unless meta[k].nil? }
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Internal helper for the tracer pipeline.
|
|
134
|
+
# @api private
|
|
135
|
+
def self.print_dependency_summary(stdout, meta, run_dir)
|
|
136
|
+
deps_path = File.join(run_dir, 'dependency.json')
|
|
137
|
+
return unless File.file?(deps_path)
|
|
138
|
+
|
|
139
|
+
deps = read_json(deps_path)
|
|
140
|
+
id = meta['example_id'] || meta['id']
|
|
141
|
+
files = Array(deps[id])
|
|
142
|
+
stdout.puts "dependencies: #{files.size} files tracked"
|
|
143
|
+
files.first(10).each { |f| stdout.puts " - #{f}" }
|
|
144
|
+
stdout.puts " ... (#{files.size - 10} more)" if files.size > 10
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|