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
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
|