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,693 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'set'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
require_relative 'backend'
|
|
9
|
+
require_relative 'lazy_snapshot'
|
|
10
|
+
require_relative 'schema'
|
|
11
|
+
require_relative 'snapshot'
|
|
12
|
+
|
|
13
|
+
module RSpecTracer
|
|
14
|
+
# Internal Storage — see {RSpecTracer} for the user-facing surface.
|
|
15
|
+
# @api private
|
|
16
|
+
module Storage
|
|
17
|
+
# SQLite-on-disk storage backend. Single file
|
|
18
|
+
# `cache_path/rspec_tracer.sqlite3` with a normalized 9-table
|
|
19
|
+
# schema so a warm run can materialize only the rows a given
|
|
20
|
+
# field needs (Filter.select hits ~50-500 example_ids out of
|
|
21
|
+
# millions, JsonBackend eagerly read the whole map). Breaks
|
|
22
|
+
# the RAM-scales-with-cache-size curve that JsonBackend cannot
|
|
23
|
+
# escape without a different on-disk shape.
|
|
24
|
+
#
|
|
25
|
+
# MRI-only. The `sqlite3` gem targets MRI's C API; JRuby's
|
|
26
|
+
# `jdbc-sqlite3` has a different API that this backend does
|
|
27
|
+
# not target in 2.0. Users who select `:sqlite` on JRuby
|
|
28
|
+
# (or on MRI without the `sqlite3` gem in their Gemfile) see
|
|
29
|
+
# the construct raise `SqliteBackendError`, which the Engine's
|
|
30
|
+
# backend dispatch converts to a warn + cold-run fallback.
|
|
31
|
+
#
|
|
32
|
+
# No multi-run history. SqliteBackend stores only the latest
|
|
33
|
+
# run; save_graph full-replaces every table inside a single
|
|
34
|
+
# `BEGIN IMMEDIATE` transaction. JsonBackend's
|
|
35
|
+
# `cache_retention_local_count` is a no-op here. Users who
|
|
36
|
+
# need rollback history stay on `:json`.
|
|
37
|
+
#
|
|
38
|
+
# `journal_mode = MEMORY` so SQLite's WAL / SHM sidecars do not
|
|
39
|
+
# leak into the user-facing `rspec_tracer_cache/` directory
|
|
40
|
+
# (USER_FACING_SURFACE.md section 6 locks the layout; sidecars
|
|
41
|
+
# would surprise debug scripts that walk the dir expecting only
|
|
42
|
+
# documented files).
|
|
43
|
+
# rubocop:disable Metrics/ClassLength
|
|
44
|
+
class SqliteBackend
|
|
45
|
+
# Raised when the sqlite3 gem cannot be loaded. Engine's
|
|
46
|
+
# build_storage_backend dispatch rescues + falls back to
|
|
47
|
+
# `:json` with a warn line - same optional-dep posture as
|
|
48
|
+
# RedisBackend uses for the redis gem.
|
|
49
|
+
class SqliteBackendError < StandardError; end
|
|
50
|
+
|
|
51
|
+
# Internal constant.
|
|
52
|
+
# @api private
|
|
53
|
+
DB_FILENAME = 'rspec_tracer.sqlite3'
|
|
54
|
+
# Internal constant.
|
|
55
|
+
# @api private
|
|
56
|
+
JOURNAL_MODE_SQL = 'PRAGMA journal_mode = MEMORY'
|
|
57
|
+
# Internal constant.
|
|
58
|
+
# @api private
|
|
59
|
+
SYNCHRONOUS_SQL = 'PRAGMA synchronous = NORMAL'
|
|
60
|
+
|
|
61
|
+
# Two concurrent save_graph calls (parallel_tests workers, fork-
|
|
62
|
+
# based test harnesses) both reach `BEGIN IMMEDIATE` and contend
|
|
63
|
+
# on SQLite's RESERVED write lock. Without busy_timeout, the
|
|
64
|
+
# losing writer raises SQLite3::BusyException immediately. 5000
|
|
65
|
+
# ms gives ~5x margin over a worst-case 1 s save on large caches
|
|
66
|
+
# (cache_load benchmark p50 ~0.6 s at 500 examples), preserving
|
|
67
|
+
# the storage layer's "concurrent writers serialize cleanly"
|
|
68
|
+
# contract verified in spec/edge_cases/concurrent_write_spec.rb.
|
|
69
|
+
BUSY_TIMEOUT_MS = 5_000
|
|
70
|
+
|
|
71
|
+
# SQLite's on-disk file format starts with the literal bytes
|
|
72
|
+
# `SQLite format 3\000` (16 bytes). A file under db_path whose
|
|
73
|
+
# leading bytes differ is not a valid database - corrupted bytes,
|
|
74
|
+
# zero-length file, or a foreign file collision. Per the
|
|
75
|
+
# architecture's graceful-degradation rule (load_graph never
|
|
76
|
+
# raises; finalize writes a fresh cache), save_graph self-heals
|
|
77
|
+
# by deleting the corrupt file before opening, so the subsequent
|
|
78
|
+
# Database.new + ensure_schema! sequence writes a fresh db.
|
|
79
|
+
# Without this, configure_connection's PRAGMA raises
|
|
80
|
+
# SQLite3::NotADatabaseException and the user is stuck unless
|
|
81
|
+
# they manually rm the cache. The `cache_loader_fuzz` harness
|
|
82
|
+
# surfaced this gap.
|
|
83
|
+
SQLITE_MAGIC_BYTES = "SQLite format 3\x00".b.freeze
|
|
84
|
+
private_constant :SQLITE_MAGIC_BYTES
|
|
85
|
+
|
|
86
|
+
# Internal constant.
|
|
87
|
+
# @api private
|
|
88
|
+
STATUS_FIELDS = {
|
|
89
|
+
interrupted_examples: 'interrupted',
|
|
90
|
+
flaky_examples: 'flaky',
|
|
91
|
+
failed_examples: 'failed',
|
|
92
|
+
pending_examples: 'pending',
|
|
93
|
+
skipped_examples: 'skipped'
|
|
94
|
+
}.freeze
|
|
95
|
+
|
|
96
|
+
# Internal constant.
|
|
97
|
+
# @api private
|
|
98
|
+
DIGEST_MAP_KINDS = {
|
|
99
|
+
boot_set: 'boot',
|
|
100
|
+
wsi_snapshot: 'wsi',
|
|
101
|
+
env_snapshot: 'env'
|
|
102
|
+
}.freeze
|
|
103
|
+
|
|
104
|
+
# Mirrors FIELD_FILENAMES from JsonBackend - the set of Snapshot
|
|
105
|
+
# fields a LazySnapshot reader may ask about. Used by
|
|
106
|
+
# SqliteFieldReader to reject unknown fields before hitting the
|
|
107
|
+
# DB. Kept in step with Snapshot.members minus the envelope.
|
|
108
|
+
READABLE_FIELDS = (
|
|
109
|
+
%i[all_examples duplicate_examples all_files dependency
|
|
110
|
+
reverse_dependency examples_coverage env_dependency cache_hit_reason
|
|
111
|
+
filtered_examples] +
|
|
112
|
+
STATUS_FIELDS.keys + DIGEST_MAP_KINDS.keys
|
|
113
|
+
).freeze
|
|
114
|
+
|
|
115
|
+
# Binds a SqliteBackend so LazySnapshot readers can dispatch
|
|
116
|
+
# one field at a time without threading a connection through.
|
|
117
|
+
class SqliteFieldReader
|
|
118
|
+
# Internal method on the tracer pipeline.
|
|
119
|
+
# @api private
|
|
120
|
+
def initialize(backend:)
|
|
121
|
+
@backend = backend
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Internal method on the tracer pipeline.
|
|
125
|
+
# @api private
|
|
126
|
+
def read(field)
|
|
127
|
+
@backend.read_field(field)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Internal attribute.
|
|
132
|
+
# @api private
|
|
133
|
+
attr_reader :cache_path
|
|
134
|
+
|
|
135
|
+
# Internal method on the tracer pipeline.
|
|
136
|
+
# @api private
|
|
137
|
+
def initialize(cache_path:, logger: nil)
|
|
138
|
+
@cache_path = File.expand_path(cache_path)
|
|
139
|
+
@logger = logger
|
|
140
|
+
load_sqlite_driver!
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Internal method on the tracer pipeline.
|
|
144
|
+
# @api private
|
|
145
|
+
def last_run_id
|
|
146
|
+
with_connection do |db|
|
|
147
|
+
row = db.get_first_row('SELECT run_id FROM meta LIMIT 1')
|
|
148
|
+
run_id = row&.first
|
|
149
|
+
return nil if run_id.nil? || run_id.to_s.empty?
|
|
150
|
+
|
|
151
|
+
run_id
|
|
152
|
+
end
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Internal method on the tracer pipeline.
|
|
158
|
+
# @api private
|
|
159
|
+
def load_graph(schema_version:)
|
|
160
|
+
with_connection do |db|
|
|
161
|
+
return nil unless meta_table_exists?(db)
|
|
162
|
+
|
|
163
|
+
row = db.get_first_row('SELECT schema_version, run_id FROM meta LIMIT 1')
|
|
164
|
+
return nil if row.nil?
|
|
165
|
+
|
|
166
|
+
stored_sv, run_id = row
|
|
167
|
+
unless Schema.supported?(stored_sv) && stored_sv == schema_version
|
|
168
|
+
info("schema_version mismatch (stored=#{stored_sv.inspect}, expected=#{schema_version}); cold run")
|
|
169
|
+
return nil
|
|
170
|
+
end
|
|
171
|
+
return nil if run_id.nil? || run_id.to_s.empty?
|
|
172
|
+
|
|
173
|
+
LazySnapshot.new(
|
|
174
|
+
schema_version: stored_sv, run_id: run_id,
|
|
175
|
+
reader: SqliteFieldReader.new(backend: self)
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
info("failed to load sqlite cache: #{e.class}: #{e.message}; cold run")
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Internal method on the tracer pipeline.
|
|
184
|
+
# @api private
|
|
185
|
+
def save_graph(snapshot, schema_version:)
|
|
186
|
+
raise ArgumentError, 'snapshot must not be nil' if snapshot.nil?
|
|
187
|
+
|
|
188
|
+
unless Schema.supported?(schema_version)
|
|
189
|
+
raise ArgumentError, "unsupported schema_version: #{schema_version.inspect}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
run_id = snapshot.run_id
|
|
193
|
+
raise ArgumentError, 'snapshot.run_id must be a non-empty string' if run_id.nil? || run_id.to_s.empty?
|
|
194
|
+
|
|
195
|
+
transactional_save do
|
|
196
|
+
db = @active_db
|
|
197
|
+
write_all_tables(db, snapshot, schema_version: schema_version)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
snapshot
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Internal method on the tracer pipeline.
|
|
204
|
+
# @api private
|
|
205
|
+
def clear!
|
|
206
|
+
return unless File.directory?(@cache_path)
|
|
207
|
+
|
|
208
|
+
FileUtils.rm_rf(@cache_path)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Internal method on the tracer pipeline.
|
|
212
|
+
# @api private
|
|
213
|
+
def transactional_save(&block)
|
|
214
|
+
raise ArgumentError, 'block required' unless block
|
|
215
|
+
|
|
216
|
+
FileUtils.mkdir_p(@cache_path)
|
|
217
|
+
with_write_connection do |db|
|
|
218
|
+
@active_db = db
|
|
219
|
+
db.transaction(:immediate, &block)
|
|
220
|
+
ensure
|
|
221
|
+
@active_db = nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Read one field on behalf of a LazySnapshot. Missing table or
|
|
226
|
+
# empty result returns the per-field empty default (Set for
|
|
227
|
+
# id-set fields, {} for hashes) so partial caches behave
|
|
228
|
+
# identically to JsonBackend under the same conditions.
|
|
229
|
+
# ArgumentError on an unknown field is a programmer mistake and
|
|
230
|
+
# propagates; the StandardError rescue is only for wire / I/O
|
|
231
|
+
# failures on the DB itself.
|
|
232
|
+
def read_field(field)
|
|
233
|
+
raise ArgumentError, "unknown snapshot field: #{field.inspect}" unless READABLE_FIELDS.include?(field)
|
|
234
|
+
|
|
235
|
+
begin
|
|
236
|
+
with_connection { |db| dispatch_read(db, field) }
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
info("sqlite read_field(#{field.inspect}) failed: #{e.class}: #{e.message}; returning empty default")
|
|
239
|
+
empty_default_for(field)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
# Internal method on the tracer pipeline.
|
|
246
|
+
# @api private
|
|
247
|
+
def db_path
|
|
248
|
+
File.join(@cache_path, DB_FILENAME)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Internal method on the tracer pipeline.
|
|
252
|
+
# @api private
|
|
253
|
+
def load_sqlite_driver!
|
|
254
|
+
require 'sqlite3'
|
|
255
|
+
rescue LoadError
|
|
256
|
+
raise SqliteBackendError,
|
|
257
|
+
"sqlite3 gem is not installed; add `gem 'sqlite3'` to your Gemfile " \
|
|
258
|
+
'(MRI only; pin `~> 1.7` on Ruby 3.1 or `~> 2.0` on Ruby >= 3.2) ' \
|
|
259
|
+
'or switch to `storage_backend :json`.'
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Internal method on the tracer pipeline.
|
|
263
|
+
# @api private
|
|
264
|
+
def with_connection
|
|
265
|
+
return nil unless File.file?(db_path)
|
|
266
|
+
|
|
267
|
+
db = ::SQLite3::Database.new(db_path)
|
|
268
|
+
configure_connection(db)
|
|
269
|
+
yield db
|
|
270
|
+
ensure
|
|
271
|
+
db&.close
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Internal method on the tracer pipeline.
|
|
275
|
+
# @api private
|
|
276
|
+
def with_write_connection
|
|
277
|
+
FileUtils.mkdir_p(@cache_path)
|
|
278
|
+
reset_corrupt_db_file!
|
|
279
|
+
db = ::SQLite3::Database.new(db_path)
|
|
280
|
+
configure_connection(db)
|
|
281
|
+
ensure_schema!(db)
|
|
282
|
+
yield db
|
|
283
|
+
ensure
|
|
284
|
+
db&.close
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Probe db_path's leading bytes against SQLite's magic header.
|
|
288
|
+
# Random bytes / zero-length / foreign collision -> delete so
|
|
289
|
+
# save_graph can write fresh. Cheap (one 16-byte read); avoids
|
|
290
|
+
# the SQLite3::NotADatabaseException path on configure_connection.
|
|
291
|
+
def reset_corrupt_db_file!
|
|
292
|
+
return unless File.file?(db_path)
|
|
293
|
+
return if File.size(db_path) >= SQLITE_MAGIC_BYTES.bytesize &&
|
|
294
|
+
File.binread(db_path, SQLITE_MAGIC_BYTES.bytesize) == SQLITE_MAGIC_BYTES
|
|
295
|
+
|
|
296
|
+
info("sqlite: cache file at #{db_path} is not a valid SQLite database; resetting for cold-cache rewrite")
|
|
297
|
+
File.delete(db_path)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Internal method on the tracer pipeline.
|
|
301
|
+
# @api private
|
|
302
|
+
def configure_connection(db)
|
|
303
|
+
# Set busy_timeout FIRST. journal_mode = MEMORY itself requires
|
|
304
|
+
# the file's write lock to switch modes; if another connection is
|
|
305
|
+
# mid-transaction (BEGIN IMMEDIATE held), the PRAGMA contends on
|
|
306
|
+
# that lock and raises SQLite3::BusyException unless busy_timeout
|
|
307
|
+
# has been configured. CI on Ruby 3.3 surfaced this: the
|
|
308
|
+
# losing concurrent writer never reached its BEGIN IMMEDIATE
|
|
309
|
+
# because configure_connection's PRAGMA failed first.
|
|
310
|
+
db.busy_timeout = BUSY_TIMEOUT_MS
|
|
311
|
+
db.execute(JOURNAL_MODE_SQL)
|
|
312
|
+
db.execute(SYNCHRONOUS_SQL)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Internal method on the tracer pipeline.
|
|
316
|
+
# @api private
|
|
317
|
+
def meta_table_exists?(db)
|
|
318
|
+
row = db.get_first_row(
|
|
319
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta' LIMIT 1"
|
|
320
|
+
)
|
|
321
|
+
!row.nil?
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Internal method on the tracer pipeline.
|
|
325
|
+
# @api private
|
|
326
|
+
def ensure_schema!(db)
|
|
327
|
+
SCHEMA_STATEMENTS.each { |sql| db.execute(sql) }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Internal constant.
|
|
331
|
+
# @api private
|
|
332
|
+
SCHEMA_STATEMENTS = [
|
|
333
|
+
'CREATE TABLE IF NOT EXISTS meta (' \
|
|
334
|
+
'schema_version INTEGER NOT NULL, ' \
|
|
335
|
+
'run_id TEXT NOT NULL, ' \
|
|
336
|
+
'timestamp TEXT NOT NULL' \
|
|
337
|
+
')',
|
|
338
|
+
'CREATE TABLE IF NOT EXISTS examples (' \
|
|
339
|
+
'example_id TEXT PRIMARY KEY, ' \
|
|
340
|
+
'metadata_json BLOB NOT NULL' \
|
|
341
|
+
')',
|
|
342
|
+
'CREATE TABLE IF NOT EXISTS duplicate_examples (' \
|
|
343
|
+
'example_id TEXT NOT NULL, ' \
|
|
344
|
+
'idx INTEGER NOT NULL, ' \
|
|
345
|
+
'entry_json BLOB NOT NULL, ' \
|
|
346
|
+
'PRIMARY KEY (example_id, idx)' \
|
|
347
|
+
')',
|
|
348
|
+
'CREATE TABLE IF NOT EXISTS all_files (' \
|
|
349
|
+
'file_name TEXT PRIMARY KEY, ' \
|
|
350
|
+
'file_path TEXT NOT NULL, ' \
|
|
351
|
+
'digest TEXT NOT NULL' \
|
|
352
|
+
')',
|
|
353
|
+
'CREATE TABLE IF NOT EXISTS dependency (' \
|
|
354
|
+
'example_id TEXT NOT NULL, ' \
|
|
355
|
+
'file_name TEXT NOT NULL, ' \
|
|
356
|
+
'PRIMARY KEY (example_id, file_name)' \
|
|
357
|
+
')',
|
|
358
|
+
'CREATE INDEX IF NOT EXISTS idx_dependency_file_name ON dependency(file_name)',
|
|
359
|
+
'CREATE TABLE IF NOT EXISTS examples_coverage (' \
|
|
360
|
+
'example_id TEXT NOT NULL, ' \
|
|
361
|
+
'file_path TEXT NOT NULL, ' \
|
|
362
|
+
'line_key INTEGER NOT NULL, ' \
|
|
363
|
+
'strength INTEGER NOT NULL, ' \
|
|
364
|
+
'PRIMARY KEY (example_id, file_path, line_key)' \
|
|
365
|
+
')',
|
|
366
|
+
'CREATE TABLE IF NOT EXISTS env_dependency (' \
|
|
367
|
+
'example_id TEXT NOT NULL, ' \
|
|
368
|
+
'env_name TEXT NOT NULL, ' \
|
|
369
|
+
'PRIMARY KEY (example_id, env_name)' \
|
|
370
|
+
')',
|
|
371
|
+
'CREATE TABLE IF NOT EXISTS digest_maps (' \
|
|
372
|
+
'kind TEXT NOT NULL, ' \
|
|
373
|
+
'key TEXT NOT NULL, ' \
|
|
374
|
+
'digest TEXT NOT NULL, ' \
|
|
375
|
+
'PRIMARY KEY (kind, key)' \
|
|
376
|
+
')',
|
|
377
|
+
'CREATE TABLE IF NOT EXISTS id_sets (' \
|
|
378
|
+
'status TEXT NOT NULL, ' \
|
|
379
|
+
'example_id TEXT NOT NULL, ' \
|
|
380
|
+
'PRIMARY KEY (status, example_id)' \
|
|
381
|
+
')'
|
|
382
|
+
].freeze
|
|
383
|
+
private_constant :SCHEMA_STATEMENTS
|
|
384
|
+
|
|
385
|
+
# Internal constant.
|
|
386
|
+
# @api private
|
|
387
|
+
TRUNCATE_TABLES = %w[
|
|
388
|
+
meta examples duplicate_examples all_files dependency
|
|
389
|
+
examples_coverage env_dependency digest_maps id_sets
|
|
390
|
+
].freeze
|
|
391
|
+
private_constant :TRUNCATE_TABLES
|
|
392
|
+
|
|
393
|
+
# Internal method on the tracer pipeline.
|
|
394
|
+
# @api private
|
|
395
|
+
def write_all_tables(db, snapshot, schema_version:)
|
|
396
|
+
TRUNCATE_TABLES.each { |t| db.execute("DELETE FROM #{t}") }
|
|
397
|
+
|
|
398
|
+
db.execute(
|
|
399
|
+
'INSERT INTO meta (schema_version, run_id, timestamp) VALUES (?, ?, ?)',
|
|
400
|
+
[schema_version, snapshot.run_id, Time.now.utc.iso8601]
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
write_example_rows(db, snapshot)
|
|
404
|
+
write_graph_rows(db, snapshot)
|
|
405
|
+
write_grouped_rows(db, snapshot)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Internal method on the tracer pipeline.
|
|
409
|
+
# @api private
|
|
410
|
+
def write_example_rows(db, snapshot)
|
|
411
|
+
insert_examples(db, snapshot.all_examples || {})
|
|
412
|
+
insert_duplicate_examples(db, snapshot.duplicate_examples || {})
|
|
413
|
+
insert_all_files(db, snapshot.all_files || {})
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Internal method on the tracer pipeline.
|
|
417
|
+
# @api private
|
|
418
|
+
def write_graph_rows(db, snapshot)
|
|
419
|
+
insert_dependency(db, snapshot.dependency || {})
|
|
420
|
+
insert_examples_coverage(db, snapshot.examples_coverage || {})
|
|
421
|
+
insert_env_dependency(db, snapshot.env_dependency || {})
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Internal method on the tracer pipeline.
|
|
425
|
+
# @api private
|
|
426
|
+
def write_grouped_rows(db, snapshot)
|
|
427
|
+
DIGEST_MAP_KINDS.each do |field, kind|
|
|
428
|
+
insert_digest_map(db, kind, snapshot.send(field) || {})
|
|
429
|
+
end
|
|
430
|
+
STATUS_FIELDS.each do |field, status|
|
|
431
|
+
insert_id_set(db, status, snapshot.send(field) || Set.new)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Internal method on the tracer pipeline.
|
|
436
|
+
# @api private
|
|
437
|
+
def insert_examples(db, examples)
|
|
438
|
+
sql = 'INSERT INTO examples (example_id, metadata_json) VALUES (?, ?)'
|
|
439
|
+
examples.each { |id, meta| db.execute(sql, [id.to_s, ::JSON.generate(meta || {})]) }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Internal method on the tracer pipeline.
|
|
443
|
+
# @api private
|
|
444
|
+
def insert_duplicate_examples(db, dupes)
|
|
445
|
+
sql = 'INSERT INTO duplicate_examples (example_id, idx, entry_json) VALUES (?, ?, ?)'
|
|
446
|
+
dupes.each do |id, list|
|
|
447
|
+
Array(list).each_with_index do |entry, idx|
|
|
448
|
+
db.execute(sql, [id.to_s, idx, ::JSON.generate(entry || {})])
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Internal method on the tracer pipeline.
|
|
454
|
+
# @api private
|
|
455
|
+
def insert_all_files(db, all_files)
|
|
456
|
+
sql = 'INSERT INTO all_files (file_name, file_path, digest) VALUES (?, ?, ?)'
|
|
457
|
+
all_files.each do |file_name, meta|
|
|
458
|
+
next unless meta.is_a?(Hash)
|
|
459
|
+
|
|
460
|
+
db.execute(sql, [file_name.to_s, fetch_hash(meta, :file_path).to_s, fetch_hash(meta, :digest).to_s])
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Internal method on the tracer pipeline.
|
|
465
|
+
# @api private
|
|
466
|
+
def insert_dependency(db, dependency)
|
|
467
|
+
sql = 'INSERT INTO dependency (example_id, file_name) VALUES (?, ?)'
|
|
468
|
+
dependency.each do |id, file_names|
|
|
469
|
+
Array(file_names).each { |name| db.execute(sql, [id.to_s, name.to_s]) }
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Internal method on the tracer pipeline.
|
|
474
|
+
# @api private
|
|
475
|
+
def insert_examples_coverage(db, coverage)
|
|
476
|
+
sql = 'INSERT INTO examples_coverage (example_id, file_path, line_key, strength) VALUES (?, ?, ?, ?)'
|
|
477
|
+
coverage.each do |id, per_file|
|
|
478
|
+
next unless per_file.is_a?(Hash)
|
|
479
|
+
|
|
480
|
+
per_file.each do |file_path, lines|
|
|
481
|
+
iterate_line_entries(lines) do |line_key, strength|
|
|
482
|
+
db.execute(sql, [id.to_s, file_path.to_s, line_key, strength])
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Accept both Hash[line_key => strength] (engine output) and
|
|
489
|
+
# Array[strength_or_nil] (raw ::Coverage.result shape) so specs
|
|
490
|
+
# that construct snapshots directly with Array values still
|
|
491
|
+
# round-trip. The canonical on-disk shape is (line_key_int,
|
|
492
|
+
# strength_int) rows; nil strengths drop out.
|
|
493
|
+
def iterate_line_entries(lines)
|
|
494
|
+
case lines
|
|
495
|
+
when Hash
|
|
496
|
+
lines.each { |line_key, strength| yield(line_key.to_i, strength.to_i) if strength }
|
|
497
|
+
when Array
|
|
498
|
+
lines.each_with_index { |strength, idx| yield(idx, strength.to_i) if strength }
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Internal method on the tracer pipeline.
|
|
503
|
+
# @api private
|
|
504
|
+
def insert_env_dependency(db, env_dep)
|
|
505
|
+
sql = 'INSERT INTO env_dependency (example_id, env_name) VALUES (?, ?)'
|
|
506
|
+
env_dep.each do |id, names|
|
|
507
|
+
Array(names).each { |name| db.execute(sql, [id.to_s, name.to_s]) }
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Internal method on the tracer pipeline.
|
|
512
|
+
# @api private
|
|
513
|
+
def insert_digest_map(db, kind, map)
|
|
514
|
+
sql = 'INSERT INTO digest_maps (kind, key, digest) VALUES (?, ?, ?)'
|
|
515
|
+
map.each { |k, digest| db.execute(sql, [kind, k.to_s, digest.to_s]) }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Internal method on the tracer pipeline.
|
|
519
|
+
# @api private
|
|
520
|
+
def insert_id_set(db, status, ids)
|
|
521
|
+
sql = 'INSERT INTO id_sets (status, example_id) VALUES (?, ?)'
|
|
522
|
+
Array(ids.to_a).each { |id| db.execute(sql, [status, id.to_s]) }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Internal method on the tracer pipeline.
|
|
526
|
+
# @api private
|
|
527
|
+
def dispatch_read(db, field)
|
|
528
|
+
case field
|
|
529
|
+
when :all_examples then read_all_examples(db)
|
|
530
|
+
when :duplicate_examples then read_duplicate_examples(db)
|
|
531
|
+
when :all_files then read_all_files(db)
|
|
532
|
+
when :dependency then read_dependency(db)
|
|
533
|
+
when :reverse_dependency then read_reverse_dependency(db)
|
|
534
|
+
when :examples_coverage then read_examples_coverage(db)
|
|
535
|
+
when :env_dependency then read_env_dependency(db)
|
|
536
|
+
when :cache_hit_reason, :filtered_examples
|
|
537
|
+
# JSON-backend-only surfaces; sqlite no-op. SqliteBackend
|
|
538
|
+
# does not persist these aggregates (would require meta-
|
|
539
|
+
# table columns / schema bump). SqliteBackend also has no
|
|
540
|
+
# parallel-tests peer-merge path (that's JsonBackend-only),
|
|
541
|
+
# so the per-id filtered_examples that backs cache_hit_reason
|
|
542
|
+
# isn't needed either. Empty default mirrors the missing-
|
|
543
|
+
# file-coerces-to-{} contract used by JsonBackend's
|
|
544
|
+
# per-field reads.
|
|
545
|
+
{}
|
|
546
|
+
else dispatch_read_grouped(db, field)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Internal method on the tracer pipeline.
|
|
551
|
+
# @api private
|
|
552
|
+
def dispatch_read_grouped(db, field)
|
|
553
|
+
return read_id_set(db, STATUS_FIELDS.fetch(field)) if STATUS_FIELDS.key?(field)
|
|
554
|
+
|
|
555
|
+
read_digest_map(db, DIGEST_MAP_KINDS.fetch(field))
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Internal method on the tracer pipeline.
|
|
559
|
+
# @api private
|
|
560
|
+
def read_all_examples(db)
|
|
561
|
+
result = {}
|
|
562
|
+
db.execute('SELECT example_id, metadata_json FROM examples').each do |row|
|
|
563
|
+
id, meta_json = row
|
|
564
|
+
result[id] = decode_hash_with_sym_keys(meta_json)
|
|
565
|
+
end
|
|
566
|
+
result
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Internal method on the tracer pipeline.
|
|
570
|
+
# @api private
|
|
571
|
+
def read_duplicate_examples(db)
|
|
572
|
+
result = Hash.new { |h, k| h[k] = [] }
|
|
573
|
+
rows = db.execute('SELECT example_id, idx, entry_json FROM duplicate_examples ORDER BY example_id, idx')
|
|
574
|
+
rows.each do |row|
|
|
575
|
+
id, _idx, entry_json = row
|
|
576
|
+
result[id] << decode_hash_with_sym_keys(entry_json)
|
|
577
|
+
end
|
|
578
|
+
result.each_with_object({}) { |(k, v), h| h[k] = v } # strip default_proc
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Internal method on the tracer pipeline.
|
|
582
|
+
# @api private
|
|
583
|
+
def read_all_files(db)
|
|
584
|
+
result = {}
|
|
585
|
+
db.execute('SELECT file_name, file_path, digest FROM all_files').each do |row|
|
|
586
|
+
name, path, digest = row
|
|
587
|
+
result[name] = { file_name: name, file_path: path, digest: digest }
|
|
588
|
+
end
|
|
589
|
+
result
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Internal method on the tracer pipeline.
|
|
593
|
+
# @api private
|
|
594
|
+
def read_dependency(db)
|
|
595
|
+
result = Hash.new { |h, k| h[k] = Set.new }
|
|
596
|
+
db.execute('SELECT example_id, file_name FROM dependency').each do |row|
|
|
597
|
+
id, name = row
|
|
598
|
+
result[id] << name
|
|
599
|
+
end
|
|
600
|
+
result.each_with_object({}) { |(k, v), h| h[k] = v }
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Internal method on the tracer pipeline.
|
|
604
|
+
# @api private
|
|
605
|
+
def read_reverse_dependency(db)
|
|
606
|
+
result = Hash.new { |h, k| h[k] = Set.new }
|
|
607
|
+
db.execute('SELECT file_name, example_id FROM dependency').each do |row|
|
|
608
|
+
name, id = row
|
|
609
|
+
result[name] << id
|
|
610
|
+
end
|
|
611
|
+
result.each_with_object({}) { |(k, v), h| h[k] = v }
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Internal method on the tracer pipeline.
|
|
615
|
+
# @api private
|
|
616
|
+
def read_examples_coverage(db)
|
|
617
|
+
result = {}
|
|
618
|
+
rows = db.execute('SELECT example_id, file_path, line_key, strength FROM examples_coverage')
|
|
619
|
+
rows.each do |row|
|
|
620
|
+
id, path, line_key, strength = row
|
|
621
|
+
result[id] ||= {}
|
|
622
|
+
(result[id][path] ||= {})[line_key.to_i] = strength.to_i
|
|
623
|
+
end
|
|
624
|
+
result
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Internal method on the tracer pipeline.
|
|
628
|
+
# @api private
|
|
629
|
+
def read_env_dependency(db)
|
|
630
|
+
result = Hash.new { |h, k| h[k] = [] }
|
|
631
|
+
rows = db.execute('SELECT example_id, env_name FROM env_dependency ORDER BY example_id, env_name')
|
|
632
|
+
rows.each do |row|
|
|
633
|
+
id, name = row
|
|
634
|
+
result[id] << name
|
|
635
|
+
end
|
|
636
|
+
result.each_with_object({}) { |(k, v), h| h[k] = v }
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Internal method on the tracer pipeline.
|
|
640
|
+
# @api private
|
|
641
|
+
def read_id_set(db, status)
|
|
642
|
+
result = Set.new
|
|
643
|
+
db.execute('SELECT example_id FROM id_sets WHERE status = ?', [status]).each do |row|
|
|
644
|
+
result << row.first
|
|
645
|
+
end
|
|
646
|
+
result
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Internal method on the tracer pipeline.
|
|
650
|
+
# @api private
|
|
651
|
+
def read_digest_map(db, kind)
|
|
652
|
+
result = {}
|
|
653
|
+
db.execute('SELECT key, digest FROM digest_maps WHERE kind = ?', [kind]).each do |row|
|
|
654
|
+
key, digest = row
|
|
655
|
+
result[key] = digest
|
|
656
|
+
end
|
|
657
|
+
result
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Internal method on the tracer pipeline.
|
|
661
|
+
# @api private
|
|
662
|
+
def empty_default_for(field)
|
|
663
|
+
return Set.new if STATUS_FIELDS.key?(field)
|
|
664
|
+
|
|
665
|
+
{}
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Internal method on the tracer pipeline.
|
|
669
|
+
# @api private
|
|
670
|
+
def decode_hash_with_sym_keys(json_bytes)
|
|
671
|
+
parsed = ::JSON.parse(json_bytes)
|
|
672
|
+
return parsed unless parsed.is_a?(Hash)
|
|
673
|
+
|
|
674
|
+
parsed.transform_keys(&:to_sym)
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# Internal method on the tracer pipeline.
|
|
678
|
+
# @api private
|
|
679
|
+
def fetch_hash(hash, key)
|
|
680
|
+
return hash[key] if hash.key?(key)
|
|
681
|
+
|
|
682
|
+
hash[key.to_s]
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Internal method on the tracer pipeline.
|
|
686
|
+
# @api private
|
|
687
|
+
def info(message)
|
|
688
|
+
@logger&.info(message)
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
# rubocop:enable Metrics/ClassLength
|
|
692
|
+
end
|
|
693
|
+
end
|