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,439 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
require_relative 'archive'
|
|
8
|
+
require_relative 'backend'
|
|
9
|
+
require_relative 'validator'
|
|
10
|
+
|
|
11
|
+
module RSpecTracer
|
|
12
|
+
# Internal RemoteCache — see {RSpecTracer} for the user-facing surface.
|
|
13
|
+
# @api private
|
|
14
|
+
module RemoteCache
|
|
15
|
+
# Filesystem implementation of `RemoteCache::Backend`. Target is
|
|
16
|
+
# a shared directory: an NFS mount, a per-host dev cache, or a CI
|
|
17
|
+
# workspace volume. Two-tier layout mirrors `S3Backend` bit-for-bit;
|
|
18
|
+
# a LocalFs root directory can be rsync'd to/from S3 without any
|
|
19
|
+
# transform (same `cache.tar.gz` per ref, same `branch_refs.json`
|
|
20
|
+
# path, same tier prefixes).
|
|
21
|
+
#
|
|
22
|
+
# <root>/main/<sha>/[<test_suite_id>/]cache.tar.gz
|
|
23
|
+
# <root>/pr/<branch>/<sha>/[<test_suite_id>/]cache.tar.gz
|
|
24
|
+
# <root>/pr/<branch>/branch_refs.json
|
|
25
|
+
#
|
|
26
|
+
# Uploads are atomic: the archive is staged at a sibling tmp path
|
|
27
|
+
# on the same filesystem, then `File.rename`d into place. POSIX
|
|
28
|
+
# rename is atomic on same-filesystem moves, which covers every
|
|
29
|
+
# shared-mount topology LocalFs targets.
|
|
30
|
+
#
|
|
31
|
+
# Concurrent writes to the same ref: last-write-wins is correct
|
|
32
|
+
# because the archive content is a deterministic function of the
|
|
33
|
+
# local cache (two workers on the same SHA produce identical bytes).
|
|
34
|
+
# No file locking - flock is unreliable over NFS (lockd sharp
|
|
35
|
+
# edges) and buys nothing when contents match.
|
|
36
|
+
#
|
|
37
|
+
# NFS caveat: on a network filesystem, cross-node consistency is
|
|
38
|
+
# eventual. A download issued by node B immediately after an upload
|
|
39
|
+
# on node A may miss; retries converge. Document as user concern,
|
|
40
|
+
# not a backend correctness issue.
|
|
41
|
+
class LocalFsBackend
|
|
42
|
+
# Internal LocalFsBackendError — see {RSpecTracer} for the user-facing surface.
|
|
43
|
+
# @api private
|
|
44
|
+
class LocalFsBackendError < StandardError; end
|
|
45
|
+
|
|
46
|
+
# Internal constant.
|
|
47
|
+
# @api private
|
|
48
|
+
MAIN_TIER = 'main'
|
|
49
|
+
# Internal constant.
|
|
50
|
+
# @api private
|
|
51
|
+
PR_TIER = 'pr'
|
|
52
|
+
# Internal constant.
|
|
53
|
+
# @api private
|
|
54
|
+
BRANCH_REFS_FILENAME = 'branch_refs.json'
|
|
55
|
+
# Internal constant.
|
|
56
|
+
# @api private
|
|
57
|
+
LAST_RUN_FILENAME = 'last_run.json'
|
|
58
|
+
# Internal constant.
|
|
59
|
+
# @api private
|
|
60
|
+
CACHE_ARCHIVE_FILENAME = Archive::CACHE_FILENAME
|
|
61
|
+
# Internal constant.
|
|
62
|
+
# @api private
|
|
63
|
+
ENCODING = 'UTF-8'
|
|
64
|
+
|
|
65
|
+
# rubocop:disable Metrics/ParameterLists
|
|
66
|
+
def initialize(root:, branch:, default_branch:, cache_path:,
|
|
67
|
+
test_suite_id: nil, logger: nil)
|
|
68
|
+
validate_required!(root: root, branch: branch,
|
|
69
|
+
default_branch: default_branch, cache_path: cache_path)
|
|
70
|
+
|
|
71
|
+
@root = File.expand_path(root.to_s)
|
|
72
|
+
@branch = branch.to_s.chomp
|
|
73
|
+
@default_branch = default_branch.to_s.chomp
|
|
74
|
+
@test_suite_id = normalize_test_suite_id(test_suite_id)
|
|
75
|
+
@cache_path = cache_path.to_s
|
|
76
|
+
@logger = logger
|
|
77
|
+
end
|
|
78
|
+
# rubocop:enable Metrics/ParameterLists
|
|
79
|
+
|
|
80
|
+
# Download the cache for `ref` into `cache_path`. Tries the
|
|
81
|
+
# backend's own tier first; on miss, falls back to the main tier
|
|
82
|
+
# for the same ref. Validates via `schema_version` before
|
|
83
|
+
# declaring success. Returns true on validated success, false
|
|
84
|
+
# otherwise. Cleans up partially-extracted state on failure.
|
|
85
|
+
#
|
|
86
|
+
# `tree_sha:` is accepted for protocol uniformity with S3Backend
|
|
87
|
+
# but is currently a no-op: the tree-SHA secondary index is an
|
|
88
|
+
# S3-only feature. Future enhancement may extend it here; the
|
|
89
|
+
# orchestrator already forwards the kwarg.
|
|
90
|
+
def download(ref, tree_sha: nil)
|
|
91
|
+
_ = tree_sha
|
|
92
|
+
return false if blank?(ref)
|
|
93
|
+
|
|
94
|
+
tiers_to_try = [own_tier_prefix]
|
|
95
|
+
tiers_to_try << main_tier_prefix if pr_tier?
|
|
96
|
+
|
|
97
|
+
tiers_to_try.any? { |tier| try_download_from(tier, ref) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Upload the local cache to this backend's own tier under `ref`.
|
|
101
|
+
# Packs the 15-file local layout into a `cache.tar.gz` via
|
|
102
|
+
# `Archive.pack`, renames into place atomically. Raises on a
|
|
103
|
+
# malformed local cache or an I/O failure.
|
|
104
|
+
#
|
|
105
|
+
# `tree_sha:` is accepted for protocol uniformity with S3Backend
|
|
106
|
+
# (no-op here; see `download`).
|
|
107
|
+
def upload(ref, tree_sha: nil)
|
|
108
|
+
_ = tree_sha
|
|
109
|
+
raise LocalFsBackendError, 'ref is required' if blank?(ref)
|
|
110
|
+
|
|
111
|
+
run_id = read_local_run_id
|
|
112
|
+
raise LocalFsBackendError, "no local cache to upload (missing #{LAST_RUN_FILENAME})" if run_id.nil?
|
|
113
|
+
|
|
114
|
+
dest = archive_path(own_tier_prefix, ref)
|
|
115
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
116
|
+
staging = "#{dest}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
|
|
117
|
+
begin
|
|
118
|
+
Archive.pack(cache_path: @cache_path, run_id: run_id, dest_path: staging)
|
|
119
|
+
File.rename(staging, dest)
|
|
120
|
+
log_debug("uploaded cache for #{ref} to #{own_tier_prefix} (#{File.size(dest)} bytes)")
|
|
121
|
+
ensure
|
|
122
|
+
FileUtils.rm_f(staging)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Read branch_refs for the given branch. Returns `{sha => ts_epoch}`
|
|
127
|
+
# or `{}` on missing / malformed. PR tier only.
|
|
128
|
+
def branch_refs(branch_name)
|
|
129
|
+
return {} if blank?(branch_name)
|
|
130
|
+
|
|
131
|
+
path = branch_refs_path(branch_name)
|
|
132
|
+
return {} unless File.file?(path)
|
|
133
|
+
|
|
134
|
+
parsed = JSON.parse(File.read(path, encoding: ENCODING))
|
|
135
|
+
parsed.is_a?(Hash) ? parsed.transform_values(&:to_i) : {}
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
log_debug("branch_refs read failed (#{e.class}: #{e.message}); treating as empty")
|
|
138
|
+
{}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Persist branch_refs for the given branch. No-op for main-branch
|
|
142
|
+
# writes. Atomic via tmp+rename. Raises on I/O failure for PR
|
|
143
|
+
# tier.
|
|
144
|
+
def write_branch_refs(branch_name, refs)
|
|
145
|
+
return if blank?(branch_name)
|
|
146
|
+
return if branch_name.to_s.chomp == @default_branch
|
|
147
|
+
return if refs.nil? || refs.empty?
|
|
148
|
+
|
|
149
|
+
path = branch_refs_path(branch_name)
|
|
150
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
151
|
+
staging = "#{path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
|
|
152
|
+
begin
|
|
153
|
+
File.write(staging, JSON.pretty_generate(refs), encoding: ENCODING)
|
|
154
|
+
File.rename(staging, path)
|
|
155
|
+
log_debug("wrote branch_refs for #{branch_name}")
|
|
156
|
+
ensure
|
|
157
|
+
FileUtils.rm_f(staging)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Apply retention to the backend's own tier. Returns count removed.
|
|
162
|
+
# Semantics match S3Backend: count keeps newest N, duration prunes
|
|
163
|
+
# by mtime, pr_branch_ttl deletes the whole branch prefix when
|
|
164
|
+
# idle. Two or more params may be set simultaneously; all nil/0
|
|
165
|
+
# is a no-op. Never raises on partial I/O failure.
|
|
166
|
+
def prune!(count: nil, duration_seconds: nil, pr_branch_ttl_seconds: nil)
|
|
167
|
+
removed = 0
|
|
168
|
+
removed += prune_by_count!(count) if count&.positive?
|
|
169
|
+
removed += prune_by_duration!(duration_seconds) if duration_seconds&.positive?
|
|
170
|
+
removed += prune_dead_pr_branch!(pr_branch_ttl_seconds) if pr_tier? && pr_branch_ttl_seconds&.positive?
|
|
171
|
+
removed
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Cross-tier PR-branch cleanup. Enumerates every branch dir under
|
|
175
|
+
# `pr/`, applies the ttl to each, deletes branches with no ref
|
|
176
|
+
# newer than the cutoff. Returns total refs removed. No-op on
|
|
177
|
+
# nil / non-positive ttl.
|
|
178
|
+
def prune_all!(pr_branch_ttl_seconds: nil)
|
|
179
|
+
return 0 unless pr_branch_ttl_seconds&.positive?
|
|
180
|
+
|
|
181
|
+
cutoff = Time.now.to_i - pr_branch_ttl_seconds.to_i
|
|
182
|
+
pr_root = File.join(@root, PR_TIER)
|
|
183
|
+
return 0 unless File.directory?(pr_root)
|
|
184
|
+
|
|
185
|
+
branch_dirs(pr_root).sum { |branch_dir| maybe_prune_branch(branch_dir, cutoff) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Warn when the main tier has grown beyond a soft threshold and
|
|
189
|
+
# no retention is configured. Called from the orchestrator.
|
|
190
|
+
def unbounded_warning(warn_threshold: 500)
|
|
191
|
+
refs = list_refs_in_tier(MAIN_TIER)
|
|
192
|
+
return nil unless refs.length > warn_threshold
|
|
193
|
+
|
|
194
|
+
"rspec-tracer remote cache has #{refs.length} refs in #{@root}/#{MAIN_TIER}; " \
|
|
195
|
+
'configure cache_retention_count or cache_retention_duration to cap growth'
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# Internal method on the tracer pipeline.
|
|
201
|
+
# @api private
|
|
202
|
+
def blank?(value)
|
|
203
|
+
value.nil? || value.to_s.empty?
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Internal method on the tracer pipeline.
|
|
207
|
+
# @api private
|
|
208
|
+
def validate_required!(**opts)
|
|
209
|
+
opts.each do |key, value|
|
|
210
|
+
raise LocalFsBackendError, "#{key} is required" if blank?(value)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Internal method on the tracer pipeline.
|
|
215
|
+
# @api private
|
|
216
|
+
def normalize_test_suite_id(raw)
|
|
217
|
+
return nil if raw.nil?
|
|
218
|
+
|
|
219
|
+
value = raw.to_s
|
|
220
|
+
value.empty? ? nil : value
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Internal method on the tracer pipeline.
|
|
224
|
+
# @api private
|
|
225
|
+
def pr_tier?
|
|
226
|
+
@branch != @default_branch
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Internal method on the tracer pipeline.
|
|
230
|
+
# @api private
|
|
231
|
+
def own_tier_prefix
|
|
232
|
+
pr_tier? ? "#{PR_TIER}/#{@branch}" : MAIN_TIER
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Internal method on the tracer pipeline.
|
|
236
|
+
# @api private
|
|
237
|
+
def main_tier_prefix
|
|
238
|
+
MAIN_TIER
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Internal method on the tracer pipeline.
|
|
242
|
+
# @api private
|
|
243
|
+
def archive_path(tier_prefix, ref)
|
|
244
|
+
File.join(*[@root, tier_prefix, ref, @test_suite_id, CACHE_ARCHIVE_FILENAME].compact)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Internal method on the tracer pipeline.
|
|
248
|
+
# @api private
|
|
249
|
+
def ref_dir(tier_prefix, ref)
|
|
250
|
+
File.join(@root, tier_prefix, ref)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Internal method on the tracer pipeline.
|
|
254
|
+
# @api private
|
|
255
|
+
def tier_dir(tier_prefix)
|
|
256
|
+
File.join(@root, tier_prefix)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Internal method on the tracer pipeline.
|
|
260
|
+
# @api private
|
|
261
|
+
def branch_refs_path(branch_name)
|
|
262
|
+
File.join(@root, PR_TIER, branch_name.chomp, BRANCH_REFS_FILENAME)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Internal method on the tracer pipeline.
|
|
266
|
+
# @api private
|
|
267
|
+
def local_last_run_path
|
|
268
|
+
File.join(@cache_path, LAST_RUN_FILENAME)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Internal method on the tracer pipeline.
|
|
272
|
+
# @api private
|
|
273
|
+
def local_run_dir(run_id)
|
|
274
|
+
File.join(@cache_path, run_id)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Internal method on the tracer pipeline.
|
|
278
|
+
# @api private
|
|
279
|
+
def read_local_run_id
|
|
280
|
+
return nil unless File.file?(local_last_run_path)
|
|
281
|
+
|
|
282
|
+
manifest = JSON.parse(File.read(local_last_run_path, encoding: ENCODING))
|
|
283
|
+
return nil unless manifest.is_a?(Hash)
|
|
284
|
+
|
|
285
|
+
run_id = manifest['run_id']
|
|
286
|
+
return nil if blank?(run_id)
|
|
287
|
+
|
|
288
|
+
run_id
|
|
289
|
+
rescue StandardError
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Attempt extraction from (tier_prefix, ref). Returns true on
|
|
294
|
+
# validated success. Rolls back any partially-extracted state on
|
|
295
|
+
# failure so the next caller doesn't observe half-landed cache.
|
|
296
|
+
# rubocop:disable Naming/PredicateMethod
|
|
297
|
+
def try_download_from(tier_prefix, ref)
|
|
298
|
+
src = archive_path(tier_prefix, ref)
|
|
299
|
+
return false unless File.file?(src)
|
|
300
|
+
|
|
301
|
+
extract_and_validate(src, tier_prefix, ref)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Internal method on the tracer pipeline.
|
|
305
|
+
# @api private
|
|
306
|
+
def extract_and_validate(src, tier_prefix, ref)
|
|
307
|
+
begin
|
|
308
|
+
Archive.extract(archive_path: src, dest_dir: @cache_path)
|
|
309
|
+
rescue StandardError => e
|
|
310
|
+
log_debug("extract failed for #{tier_prefix}/#{ref}: #{e.class}: #{e.message}")
|
|
311
|
+
rollback_extracted_cache
|
|
312
|
+
return false
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
return true if Validator.valid_file?(local_last_run_path)
|
|
316
|
+
|
|
317
|
+
log_debug("rejected #{tier_prefix}/#{ref}: schema_version mismatch")
|
|
318
|
+
rollback_extracted_cache
|
|
319
|
+
false
|
|
320
|
+
end
|
|
321
|
+
# rubocop:enable Naming/PredicateMethod
|
|
322
|
+
|
|
323
|
+
def rollback_extracted_cache
|
|
324
|
+
run_id = read_local_run_id
|
|
325
|
+
FileUtils.rm_f(local_last_run_path)
|
|
326
|
+
FileUtils.rm_rf(local_run_dir(run_id)) if run_id
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Enumerate refs in `tier_prefix` as Array<[ref, mtime_epoch]>,
|
|
330
|
+
# newest-first. Uses the archive's mtime as the timestamp proxy -
|
|
331
|
+
# a fresh upload overwrites the archive via atomic rename so
|
|
332
|
+
# mtime tracks upload time correctly.
|
|
333
|
+
def list_refs_in_tier(tier_prefix)
|
|
334
|
+
dir = tier_dir(tier_prefix)
|
|
335
|
+
return [] unless File.directory?(dir)
|
|
336
|
+
|
|
337
|
+
refs = []
|
|
338
|
+
Dir.each_child(dir) do |ref|
|
|
339
|
+
archive = archive_path(tier_prefix, ref)
|
|
340
|
+
next unless File.file?(archive)
|
|
341
|
+
|
|
342
|
+
refs << [ref, File.mtime(archive).to_i]
|
|
343
|
+
end
|
|
344
|
+
refs.sort_by { |_, ts| -ts }
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Internal method on the tracer pipeline.
|
|
348
|
+
# @api private
|
|
349
|
+
def prune_by_count!(count)
|
|
350
|
+
refs = list_refs_in_tier(own_tier_prefix)
|
|
351
|
+
return 0 if refs.length <= count
|
|
352
|
+
|
|
353
|
+
to_delete = refs[count..] || []
|
|
354
|
+
delete_refs(to_delete.map(&:first), own_tier_prefix)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Internal method on the tracer pipeline.
|
|
358
|
+
# @api private
|
|
359
|
+
def prune_by_duration!(duration_seconds)
|
|
360
|
+
cutoff = Time.now.to_i - duration_seconds.to_i
|
|
361
|
+
stale = list_refs_in_tier(own_tier_prefix).select { |_, ts| ts < cutoff }.map(&:first)
|
|
362
|
+
delete_refs(stale, own_tier_prefix)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Internal method on the tracer pipeline.
|
|
366
|
+
# @api private
|
|
367
|
+
def delete_refs(refs, tier_prefix)
|
|
368
|
+
removed = 0
|
|
369
|
+
refs.each do |ref|
|
|
370
|
+
FileUtils.rm_rf(ref_dir(tier_prefix, ref))
|
|
371
|
+
removed += 1
|
|
372
|
+
log_debug("pruned ref #{tier_prefix}/#{ref}")
|
|
373
|
+
rescue StandardError => e
|
|
374
|
+
log_warn("failed to prune ref #{tier_prefix}/#{ref}: #{e.class}: #{e.message}")
|
|
375
|
+
end
|
|
376
|
+
removed
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Internal method on the tracer pipeline.
|
|
380
|
+
# @api private
|
|
381
|
+
def prune_dead_pr_branch!(ttl_seconds)
|
|
382
|
+
refs = list_refs_in_tier(own_tier_prefix)
|
|
383
|
+
return 0 if refs.empty?
|
|
384
|
+
|
|
385
|
+
newest_ts = refs.first[1]
|
|
386
|
+
return 0 if newest_ts >= Time.now.to_i - ttl_seconds.to_i
|
|
387
|
+
|
|
388
|
+
delete_branch_prefix(own_tier_prefix, refs.length)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Delete every descendant of `<root>/<tier_prefix>` (cache dirs +
|
|
392
|
+
# branch_refs.json). Returns the supplied `ref_count` on success,
|
|
393
|
+
# 0 on failure.
|
|
394
|
+
def delete_branch_prefix(tier_prefix, ref_count)
|
|
395
|
+
FileUtils.rm_rf(tier_dir(tier_prefix))
|
|
396
|
+
log_debug("pruned dead PR branch #{tier_prefix}")
|
|
397
|
+
ref_count
|
|
398
|
+
rescue StandardError => e
|
|
399
|
+
log_warn("failed to prune dead PR branch #{tier_prefix}: #{e.class}: #{e.message}")
|
|
400
|
+
0
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Internal method on the tracer pipeline.
|
|
404
|
+
# @api private
|
|
405
|
+
def branch_dirs(pr_root)
|
|
406
|
+
Dir.each_child(pr_root)
|
|
407
|
+
.map { |name| File.join(pr_root, name) }
|
|
408
|
+
.select { |path| File.directory?(path) }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Apply the TTL to a single PR branch dir. Deletes whole branch
|
|
412
|
+
# when its newest ref is older than cutoff. Returns the count of
|
|
413
|
+
# refs removed (0 when branch is alive).
|
|
414
|
+
def maybe_prune_branch(branch_dir, cutoff)
|
|
415
|
+
branch_name = File.basename(branch_dir)
|
|
416
|
+
tier_prefix = "#{PR_TIER}/#{branch_name}"
|
|
417
|
+
refs = list_refs_in_tier(tier_prefix)
|
|
418
|
+
return 0 if refs.empty?
|
|
419
|
+
|
|
420
|
+
newest_ts = refs.first[1]
|
|
421
|
+
return 0 if newest_ts >= cutoff
|
|
422
|
+
|
|
423
|
+
delete_branch_prefix(tier_prefix, refs.length)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Internal method on the tracer pipeline.
|
|
427
|
+
# @api private
|
|
428
|
+
def log_debug(message)
|
|
429
|
+
@logger&.debug("rspec-tracer remote_cache: #{message}")
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Internal method on the tracer pipeline.
|
|
433
|
+
# @api private
|
|
434
|
+
def log_warn(message)
|
|
435
|
+
@logger&.warn("rspec-tracer remote_cache: #{message}")
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|