testsift 0.0.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +22 -0
- data/README.md +572 -0
- data/bin/testsift +15 -0
- data/lib/testsift/cache/Rakefile +43 -0
- data/lib/testsift/cli/blast_radius.rb +318 -0
- data/lib/testsift/cli/cache_clear.rb +114 -0
- data/lib/testsift/cli/cache_info.rb +106 -0
- data/lib/testsift/cli/doctor.rb +307 -0
- data/lib/testsift/cli/explain.rb +273 -0
- data/lib/testsift/cli/report_open.rb +90 -0
- data/lib/testsift/cli/snapshot_helpers.rb +119 -0
- data/lib/testsift/cli.rb +219 -0
- data/lib/testsift/configuration.rb +1467 -0
- data/lib/testsift/defaults.rb +11 -0
- data/lib/testsift/engine/snapshot_builder.rb +117 -0
- data/lib/testsift/engine.rb +896 -0
- data/lib/testsift/example.rb +188 -0
- data/lib/testsift/filter.rb +103 -0
- data/lib/testsift/line_stub.rb +62 -0
- data/lib/testsift/load_config.rb +15 -0
- data/lib/testsift/load_default_config.rb +46 -0
- data/lib/testsift/load_global_config.rb +33 -0
- data/lib/testsift/load_local_config.rb +17 -0
- data/lib/testsift/logger.rb +59 -0
- data/lib/testsift/rails/README.md +78 -0
- data/lib/testsift/rails/i18n_tracking.rb +137 -0
- data/lib/testsift/rails/notifications.rb +263 -0
- data/lib/testsift/rails/preset.rb +94 -0
- data/lib/testsift/rails/railtie.rb +22 -0
- data/lib/testsift/rails.rb +15 -0
- data/lib/testsift/remote_cache/README.md +140 -0
- data/lib/testsift/remote_cache/Rakefile +61 -0
- data/lib/testsift/remote_cache/archive.rb +137 -0
- data/lib/testsift/remote_cache/backend.rb +73 -0
- data/lib/testsift/remote_cache/git_ancestry.rb +241 -0
- data/lib/testsift/remote_cache/local_fs_backend.rb +439 -0
- data/lib/testsift/remote_cache/redis_backend.rb +554 -0
- data/lib/testsift/remote_cache/s3_backend.rb +712 -0
- data/lib/testsift/remote_cache/user_tasks.rb +436 -0
- data/lib/testsift/remote_cache/validator.rb +50 -0
- data/lib/testsift/remote_cache.rb +22 -0
- data/lib/testsift/reporters/README.md +103 -0
- data/lib/testsift/reporters/base.rb +87 -0
- data/lib/testsift/reporters/coverage_json_reporter.rb +338 -0
- data/lib/testsift/reporters/html/.gitignore +19 -0
- data/lib/testsift/reporters/html/.prettierignore +4 -0
- data/lib/testsift/reporters/html/.prettierrc.json +9 -0
- data/lib/testsift/reporters/html/README.md +80 -0
- data/lib/testsift/reporters/html/dist/assets/index.css +2 -0
- data/lib/testsift/reporters/html/dist/assets/index.js +1 -0
- data/lib/testsift/reporters/html/dist/index.html +24 -0
- data/lib/testsift/reporters/html/eslint.config.js +62 -0
- data/lib/testsift/reporters/html/package-lock.json +4910 -0
- data/lib/testsift/reporters/html/package.json +29 -0
- data/lib/testsift/reporters/html/src/app.jsx +130 -0
- data/lib/testsift/reporters/html/src/components/AllExamples.jsx +86 -0
- data/lib/testsift/reporters/html/src/components/DuplicateExamples.jsx +68 -0
- data/lib/testsift/reporters/html/src/components/ExamplesDependency.jsx +78 -0
- data/lib/testsift/reporters/html/src/components/FilesDependency.jsx +72 -0
- data/lib/testsift/reporters/html/src/components/FlakyExamples.jsx +42 -0
- data/lib/testsift/reporters/html/src/components/ReportTable.jsx +131 -0
- data/lib/testsift/reporters/html/src/components/SearchBar.jsx +19 -0
- data/lib/testsift/reporters/html/src/index.html +23 -0
- data/lib/testsift/reporters/html/src/main.jsx +37 -0
- data/lib/testsift/reporters/html/src/styles.css +434 -0
- data/lib/testsift/reporters/html/vite.config.js +42 -0
- data/lib/testsift/reporters/html_reporter.rb +266 -0
- data/lib/testsift/reporters/json_reporter.rb +88 -0
- data/lib/testsift/reporters/payload_builder.rb +241 -0
- data/lib/testsift/reporters/registry.rb +120 -0
- data/lib/testsift/reporters/terminal_reporter.rb +264 -0
- data/lib/testsift/rspec/README.md +73 -0
- data/lib/testsift/rspec/installation.rb +97 -0
- data/lib/testsift/rspec/metadata.rb +96 -0
- data/lib/testsift/rspec/parallel_tests.rb +459 -0
- data/lib/testsift/rspec/rails_setup.rb +42 -0
- data/lib/testsift/rspec/reporter_hook.rb +90 -0
- data/lib/testsift/rspec/runner_hook.rb +253 -0
- data/lib/testsift/source_file.rb +56 -0
- data/lib/testsift/storage/README.md +35 -0
- data/lib/testsift/storage/backend.rb +130 -0
- data/lib/testsift/storage/json_backend.rb +884 -0
- data/lib/testsift/storage/lazy_snapshot.rb +65 -0
- data/lib/testsift/storage/schema.rb +50 -0
- data/lib/testsift/storage/serializer/json.rb +41 -0
- data/lib/testsift/storage/serializer/msgpack.rb +167 -0
- data/lib/testsift/storage/snapshot.rb +141 -0
- data/lib/testsift/storage/sqlite_backend.rb +709 -0
- data/lib/testsift/time_formatter.rb +74 -0
- data/lib/testsift/tracker/README.md +36 -0
- data/lib/testsift/tracker/coverage_adapter.rb +180 -0
- data/lib/testsift/tracker/declared_globs.rb +100 -0
- data/lib/testsift/tracker/dependency_graph.rb +134 -0
- data/lib/testsift/tracker/env_matcher.rb +127 -0
- data/lib/testsift/tracker/env_snapshot.rb +77 -0
- data/lib/testsift/tracker/example_registry.rb +153 -0
- data/lib/testsift/tracker/file_digest.rb +61 -0
- data/lib/testsift/tracker/filter.rb +127 -0
- data/lib/testsift/tracker/input.rb +99 -0
- data/lib/testsift/tracker/io_hooks/file.rb +55 -0
- data/lib/testsift/tracker/io_hooks/io.rb +24 -0
- data/lib/testsift/tracker/io_hooks/json.rb +23 -0
- data/lib/testsift/tracker/io_hooks/kernel.rb +26 -0
- data/lib/testsift/tracker/io_hooks/yaml.rb +38 -0
- data/lib/testsift/tracker/io_hooks.rb +195 -0
- data/lib/testsift/tracker/loaded_files_tracker.rb +290 -0
- data/lib/testsift/tracker/new_file_detector.rb +62 -0
- data/lib/testsift/tracker/whole_suite_invalidators.rb +96 -0
- data/lib/testsift/version.rb +7 -0
- data/lib/testsift.rb +335 -0
- metadata +193 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'testsift/storage/backend'
|
|
4
|
+
require 'testsift/storage/json_backend'
|
|
5
|
+
require 'testsift/storage/schema'
|
|
6
|
+
require 'testsift/storage/sqlite_backend' if RUBY_ENGINE == 'ruby'
|
|
7
|
+
|
|
8
|
+
module TestSift
|
|
9
|
+
# Internal CLI -- see {TestSift} for the user-facing surface.
|
|
10
|
+
# @api private
|
|
11
|
+
module CLI
|
|
12
|
+
# Shared plumbing for the snapshot-reading sub-commands (Explain,
|
|
13
|
+
# BlastRadius): help-flag detection, backend-agnostic snapshot
|
|
14
|
+
# loading with uniform degradation messages, and String/Symbol-
|
|
15
|
+
# tolerant metadata lookups. Extracted so the two commands cannot
|
|
16
|
+
# drift apart on message wording or cache-shape tolerance.
|
|
17
|
+
# @api private
|
|
18
|
+
module SnapshotHelpers
|
|
19
|
+
# @param args [Array<String>] sub-command args (flags removed
|
|
20
|
+
# by the caller where applicable)
|
|
21
|
+
# @return [Boolean] true when the sub-command should print its
|
|
22
|
+
# help text instead of executing
|
|
23
|
+
def self.help_requested?(args)
|
|
24
|
+
args.empty? || args.include?('-h') || args.include?('--help')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Resolve the persisted snapshot through the configured storage
|
|
28
|
+
# backend ({TestSift::Storage::Backend.build}), so
|
|
29
|
+
# `storage_backend :sqlite` resolves the latest run from the
|
|
30
|
+
# meta table instead of the JsonBackend-only `last_run.json`.
|
|
31
|
+
#
|
|
32
|
+
# @param cache_path [String] root cache directory
|
|
33
|
+
# @param command [String] sub-command name used as the message
|
|
34
|
+
# prefix (e.g. `explain`, `blast-radius`)
|
|
35
|
+
# @param stderr [IO]
|
|
36
|
+
# @return [Array(Object, Object), nil] `[snapshot, backend]` on
|
|
37
|
+
# success; nil (after printing a one-line explanation) when no
|
|
38
|
+
# run has been recorded yet or the cache schema is
|
|
39
|
+
# incompatible
|
|
40
|
+
def self.load_snapshot(cache_path, command:, stderr:)
|
|
41
|
+
backend = Storage::Backend.build(cache_path: cache_path, configuration: TestSift)
|
|
42
|
+
run_id = backend.last_run_id
|
|
43
|
+
if run_id.nil? || run_id.to_s.empty?
|
|
44
|
+
stderr.puts "#{command}: no cache yet at #{cache_path} -- run rspec first"
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
snapshot = backend.load_graph(schema_version: Storage::Schema::CURRENT)
|
|
49
|
+
if snapshot.nil?
|
|
50
|
+
stderr.puts "#{command}: cache at #{cache_path} is incompatible with this testsift; " \
|
|
51
|
+
'next rspec run is cold'
|
|
52
|
+
return nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
[snapshot, backend]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether the backend persists the per-run filter decisions
|
|
59
|
+
# (`filtered_examples` / `skipped_examples`). JsonBackend does;
|
|
60
|
+
# SqliteBackend deliberately does not (its `dispatch_read`
|
|
61
|
+
# returns `{}` for those fields). Callers use this to attribute
|
|
62
|
+
# an empty decision set honestly: on a persisting backend it
|
|
63
|
+
# means "no decision was recorded for this id" (cold run, or the
|
|
64
|
+
# id was not part of the last run), NOT a backend limitation.
|
|
65
|
+
#
|
|
66
|
+
# @param backend [Object] a {TestSift::Storage::Backend.build}
|
|
67
|
+
# product
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def self.filter_decisions_persisted?(backend)
|
|
70
|
+
backend.is_a?(Storage::JsonBackend)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Look up a key from a Hash, tolerating both String and Symbol
|
|
74
|
+
# storage. Snapshot Hashes round-tripped through JSON yield
|
|
75
|
+
# String keys; the post-#182 msgpack serializer preserves
|
|
76
|
+
# Symbol keys end-to-end, so callers can't assume either shape.
|
|
77
|
+
def self.fetch_meta(meta, *keys)
|
|
78
|
+
keys.each do |k|
|
|
79
|
+
v = meta[k]
|
|
80
|
+
return v unless v.nil?
|
|
81
|
+
|
|
82
|
+
sym_value = meta[k.to_sym]
|
|
83
|
+
return sym_value unless sym_value.nil?
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Look up a nested key from a Hash, tolerating both String and
|
|
89
|
+
# Symbol storage at each level. See {.fetch_meta} for rationale.
|
|
90
|
+
def self.dig_meta(meta, *keys)
|
|
91
|
+
keys.reduce(meta) do |acc, k|
|
|
92
|
+
break nil if acc.nil? || !acc.is_a?(::Hash)
|
|
93
|
+
|
|
94
|
+
acc[k] || acc[k.to_sym]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The example's rerun location from persisted all_examples meta,
|
|
99
|
+
# preferring the rerun_* fields the reporter records.
|
|
100
|
+
#
|
|
101
|
+
# @param meta [Hash] one all_examples entry
|
|
102
|
+
# @return [Array(Object, Object)] `[file, line]`; either element
|
|
103
|
+
# may be nil when the meta is missing the field
|
|
104
|
+
def self.example_location(meta)
|
|
105
|
+
[
|
|
106
|
+
fetch_meta(meta, 'rerun_file_name', 'file_name'),
|
|
107
|
+
fetch_meta(meta, 'rerun_line_number', 'line_number')
|
|
108
|
+
]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param meta [Hash] one all_examples entry
|
|
112
|
+
# @return [String, nil] the example's description, or nil when
|
|
113
|
+
# the meta is missing both description fields
|
|
114
|
+
def self.example_description(meta)
|
|
115
|
+
fetch_meta(meta, 'full_description', 'description')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/testsift/cli.rb
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
# Only the Logger class -- NOT the full library, whose boot would
|
|
6
|
+
# `load` the project config. {CLI.with_default_logger_out} needs the
|
|
7
|
+
# class defined before that boot so the config-load window cannot
|
|
8
|
+
# construct a stdout-bound logger.
|
|
9
|
+
require_relative 'logger'
|
|
10
|
+
|
|
11
|
+
module TestSift
|
|
12
|
+
# Command-line entry for the `testsift` binary. Six sub-commands
|
|
13
|
+
# per USER_FACING_SURFACE.md section 10: `doctor`, `cache:info`,
|
|
14
|
+
# `cache:clear`, `report:open`, `explain <example>`,
|
|
15
|
+
# `blast-radius <file>`.
|
|
16
|
+
#
|
|
17
|
+
# The CLI is opt-in — the canonical CI flow continues to go through
|
|
18
|
+
# `rake testsift:remote_cache:*` tasks per USER_FACING_SURFACE.md
|
|
19
|
+
# §5. Sub-commands operate against the local cache + report
|
|
20
|
+
# directories resolved from the project's `.testsift` config.
|
|
21
|
+
# {.run} loads the tracer library (and with it the project + global
|
|
22
|
+
# `.testsift` configs) via {.load_tracer} right before
|
|
23
|
+
# sub-command dispatch, so `--help` / `--version` work even in a
|
|
24
|
+
# project whose config is broken, and a config file that raises at
|
|
25
|
+
# load time degrades to a one-line message + exit 1 instead of a
|
|
26
|
+
# raw backtrace.
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
module CLI
|
|
30
|
+
# Internal constant.
|
|
31
|
+
# @api private
|
|
32
|
+
SUB_COMMANDS = {
|
|
33
|
+
'doctor' => 'Doctor',
|
|
34
|
+
'cache:info' => 'CacheInfo',
|
|
35
|
+
'cache:clear' => 'CacheClear',
|
|
36
|
+
'report:open' => 'ReportOpen',
|
|
37
|
+
'explain' => 'Explain',
|
|
38
|
+
'blast-radius' => 'BlastRadius'
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# CLI entry. Called by `bin/testsift` with `ARGV`. Wraps every
|
|
42
|
+
# sub-command in a top-level rescue so the binary always exits
|
|
43
|
+
# with a meaningful integer status (0 / 1) instead of a backtrace.
|
|
44
|
+
#
|
|
45
|
+
# @param argv [Array<String>] command-line arguments (excluding
|
|
46
|
+
# the program name)
|
|
47
|
+
# @param stdout [IO] stream for normal output (default `$stdout`)
|
|
48
|
+
# @param stderr [IO] stream for errors / diagnostics (default `$stderr`)
|
|
49
|
+
# @return [Integer] exit status (0 = success, 1 = failure)
|
|
50
|
+
def self.run(argv, stdout: $stdout, stderr: $stderr)
|
|
51
|
+
args = argv.dup
|
|
52
|
+
return print_top_level_help(stdout) if args.empty? || %w[-h --help help].include?(args.first)
|
|
53
|
+
return print_version(stdout) if %w[-v --version].include?(args.first)
|
|
54
|
+
|
|
55
|
+
with_default_logger_out(stderr) do
|
|
56
|
+
if load_tracer(stderr)
|
|
57
|
+
with_logger_on(stderr) { dispatch(args, stdout: stdout, stderr: stderr) }
|
|
58
|
+
else
|
|
59
|
+
1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
rescue Errno::EPIPE
|
|
63
|
+
# A downstream pipe consumer (`testsift ... | head`) closed
|
|
64
|
+
# early -- routine in shell pipelines, not a failure. Exit 0
|
|
65
|
+
# without printing: writing to stderr could raise EPIPE again
|
|
66
|
+
# when both streams share the closed pipe.
|
|
67
|
+
0
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
stderr.puts "testsift: #{e.class}: #{e.message}"
|
|
70
|
+
1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Boot the tracer library for sub-command dispatch. Loading
|
|
74
|
+
# `testsift` pulls in `docile` (used by
|
|
75
|
+
# `Configuration#configure`), the modules sub-commands rely on,
|
|
76
|
+
# AND the project's `.testsift` / `~/.testsift` configs
|
|
77
|
+
# (arbitrary user Ruby, `load`ed by `load_config`). It does NOT
|
|
78
|
+
# call `TestSift.start` - the engine stays inert until a user
|
|
79
|
+
# invokes it explicitly.
|
|
80
|
+
#
|
|
81
|
+
# A raising config must not crash the binary with a backtrace:
|
|
82
|
+
# rescue ScriptError as well as StandardError because a syntax
|
|
83
|
+
# error in `.testsift` raises SyntaxError, which is not a
|
|
84
|
+
# StandardError and would sail past {.run}'s rescue.
|
|
85
|
+
#
|
|
86
|
+
# @param stderr [IO]
|
|
87
|
+
# @return [Boolean] true when the tracer library (and the user
|
|
88
|
+
# configs it loads) booted cleanly
|
|
89
|
+
def self.load_tracer(stderr)
|
|
90
|
+
require 'testsift'
|
|
91
|
+
true
|
|
92
|
+
rescue ScriptError, StandardError => e
|
|
93
|
+
stderr.puts "testsift: could not load configuration (.testsift): #{e.class}: #{e.message}"
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Point {TestSift::Logger.default_out} at the CLI's stderr for
|
|
98
|
+
# the duration of library boot + sub-command dispatch, restoring
|
|
99
|
+
# the previous value afterwards. This must wrap {.load_tracer}:
|
|
100
|
+
# booting the library `load`s the project + global `.testsift`
|
|
101
|
+
# configs, and those can write through the logger BEFORE
|
|
102
|
+
# {.with_logger_on} ever runs -- the 1.x-compat deprecation shims
|
|
103
|
+
# (`reports_s3_path`, `use_local_aws`) fire a one-time
|
|
104
|
+
# `logger.warn` at first use, and in a fresh CLI process
|
|
105
|
+
# "one-time" means every invocation. The logger's default
|
|
106
|
+
# destination is stdout -- the stream machine consumers parse --
|
|
107
|
+
# so that warning would land ahead of e.g. the `blast-radius
|
|
108
|
+
# --json` document and break `| jq`. With the default rebound,
|
|
109
|
+
# every logger constructed during the window binds to stderr,
|
|
110
|
+
# including loggers recreated mid-load when a config sets
|
|
111
|
+
# `log_level` (which resets the memoized instance) after a
|
|
112
|
+
# deprecated DSL call. Normal `rspec` runs (whose users expect
|
|
113
|
+
# stdout logs) never enter this path and stay untouched.
|
|
114
|
+
# @api private
|
|
115
|
+
def self.with_default_logger_out(stderr)
|
|
116
|
+
previous = TestSift::Logger.default_out
|
|
117
|
+
TestSift::Logger.default_out = stderr
|
|
118
|
+
yield
|
|
119
|
+
ensure
|
|
120
|
+
TestSift::Logger.default_out = previous
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Bind the tracer's internal logger to the CLI's stderr for the
|
|
124
|
+
# duration of sub-command dispatch, restoring the previous logger
|
|
125
|
+
# afterwards. Backend diagnostics run through `TestSift.logger`
|
|
126
|
+
# (e.g. {TestSift::Storage::Backend.build}'s sqlite-unavailable
|
|
127
|
+
# fallback warning, JsonBackend's schema-mismatch info line).
|
|
128
|
+
# {.with_default_logger_out} already redirects loggers constructed
|
|
129
|
+
# during the CLI window; this layer additionally rebinds a logger
|
|
130
|
+
# that was memoized into `TestSift.@logger` BEFORE {.run} was
|
|
131
|
+
# called (possible for in-process callers -- the spec suite, or a
|
|
132
|
+
# user embedding the CLI -- where the library booted earlier with
|
|
133
|
+
# the stdout default). Sub-commands like `blast-radius --json`
|
|
134
|
+
# promise exactly one JSON document on stdout, so a diagnostic
|
|
135
|
+
# line printed ahead of it breaks `| jq`; together the two layers
|
|
136
|
+
# keep ALL diagnostics on stderr alongside the CLI's own messages.
|
|
137
|
+
#
|
|
138
|
+
# `TestSift.logger` memoizes into `@logger`
|
|
139
|
+
# (Configuration#logger); the ivar is seeded directly because
|
|
140
|
+
# Configuration deliberately exposes no logger setter -- adding
|
|
141
|
+
# one would leak it into the user-facing `configure` DSL surface.
|
|
142
|
+
# @api private
|
|
143
|
+
def self.with_logger_on(stderr)
|
|
144
|
+
previous = TestSift.instance_variable_get(:@logger)
|
|
145
|
+
TestSift.instance_variable_set(
|
|
146
|
+
:@logger, TestSift::Logger.new(TestSift.log_level, out: stderr)
|
|
147
|
+
)
|
|
148
|
+
yield
|
|
149
|
+
ensure
|
|
150
|
+
TestSift.instance_variable_set(:@logger, previous)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Internal helper for the tracer pipeline.
|
|
154
|
+
# @api private
|
|
155
|
+
def self.dispatch(args, stdout:, stderr:)
|
|
156
|
+
sub = args.shift
|
|
157
|
+
klass_name = SUB_COMMANDS[sub]
|
|
158
|
+
return unknown_sub_command(sub, stderr) if klass_name.nil?
|
|
159
|
+
|
|
160
|
+
load_sub_command(klass_name)
|
|
161
|
+
TestSift::CLI.const_get(klass_name).run(args, stdout: stdout, stderr: stderr)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Internal helper for the tracer pipeline.
|
|
165
|
+
# @api private
|
|
166
|
+
def self.unknown_sub_command(sub, stderr)
|
|
167
|
+
stderr.puts "testsift: unknown sub-command #{sub.inspect}"
|
|
168
|
+
stderr.puts " available: #{SUB_COMMANDS.keys.join(', ')}"
|
|
169
|
+
1
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Internal helper for the tracer pipeline.
|
|
173
|
+
# @api private
|
|
174
|
+
def self.print_top_level_help(stdout)
|
|
175
|
+
stdout.puts <<~HELP
|
|
176
|
+
Usage: testsift <sub-command> [options]
|
|
177
|
+
|
|
178
|
+
Sub-commands:
|
|
179
|
+
doctor Diagnose testsift config + environment.
|
|
180
|
+
cache:info Show cache size, last run timestamp, and example counts.
|
|
181
|
+
cache:clear Remove cache, coverage, and report directories.
|
|
182
|
+
report:open Open the HTML report in the default browser.
|
|
183
|
+
explain <id> Show why an example is scheduled to run or skip.
|
|
184
|
+
blast-radius <f> Show which examples a file change would re-run.
|
|
185
|
+
|
|
186
|
+
Options:
|
|
187
|
+
-h, --help Print this help message.
|
|
188
|
+
-v, --version Print testsift version.
|
|
189
|
+
|
|
190
|
+
Run `testsift <sub-command> --help` for sub-command options.
|
|
191
|
+
HELP
|
|
192
|
+
0
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Internal helper for the tracer pipeline.
|
|
196
|
+
# @api private
|
|
197
|
+
def self.print_version(stdout)
|
|
198
|
+
# Only the version constant - not the full library, whose boot
|
|
199
|
+
# would `load` the project config and could raise before the
|
|
200
|
+
# version ever printed.
|
|
201
|
+
require 'testsift/version'
|
|
202
|
+
stdout.puts "testsift #{TestSift::VERSION}"
|
|
203
|
+
0
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Internal helper for the tracer pipeline.
|
|
207
|
+
# @api private
|
|
208
|
+
def self.load_sub_command(klass_name)
|
|
209
|
+
filename = klass_name.gsub(/([A-Z])/) { |m| "_#{m.downcase}" }.sub(/^_/, '')
|
|
210
|
+
require_relative "cli/#{filename}"
|
|
211
|
+
rescue LoadError => e
|
|
212
|
+
# LoadError isn't a StandardError, so the outer `rescue StandardError`
|
|
213
|
+
# in `.run` wouldn't catch it. Re-raise as StandardError so the dispatch
|
|
214
|
+
# path stays uniform: any sub-command resolution failure prints
|
|
215
|
+
# `testsift: <class>: <message>` and exits 1.
|
|
216
|
+
raise StandardError, "could not load sub-command #{klass_name.inspect}: #{e.message}"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|