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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +384 -67
  3. data/README.md +454 -429
  4. data/bin/rspec-tracer +15 -0
  5. data/lib/rspec_tracer/cache/Rakefile +43 -0
  6. data/lib/rspec_tracer/cli/cache_clear.rb +111 -0
  7. data/lib/rspec_tracer/cli/cache_info.rb +104 -0
  8. data/lib/rspec_tracer/cli/doctor.rb +284 -0
  9. data/lib/rspec_tracer/cli/explain.rb +158 -0
  10. data/lib/rspec_tracer/cli/report_open.rb +82 -0
  11. data/lib/rspec_tracer/cli.rb +116 -0
  12. data/lib/rspec_tracer/configuration.rb +1196 -3
  13. data/lib/rspec_tracer/engine.rb +1168 -0
  14. data/lib/rspec_tracer/example.rb +141 -11
  15. data/lib/rspec_tracer/filter.rb +35 -0
  16. data/lib/rspec_tracer/line_stub.rb +61 -0
  17. data/lib/rspec_tracer/load_config.rb +2 -2
  18. data/lib/rspec_tracer/logger.rb +15 -0
  19. data/lib/rspec_tracer/rails/README.md +78 -0
  20. data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
  21. data/lib/rspec_tracer/rails/notifications.rb +263 -0
  22. data/lib/rspec_tracer/rails/preset.rb +94 -0
  23. data/lib/rspec_tracer/rails/railtie.rb +22 -0
  24. data/lib/rspec_tracer/rails.rb +15 -0
  25. data/lib/rspec_tracer/remote_cache/README.md +140 -0
  26. data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
  27. data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
  28. data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
  29. data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
  30. data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
  31. data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
  32. data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
  33. data/lib/rspec_tracer/remote_cache/user_tasks.rb +436 -0
  34. data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
  35. data/lib/rspec_tracer/remote_cache.rb +22 -0
  36. data/lib/rspec_tracer/reporters/README.md +103 -0
  37. data/lib/rspec_tracer/reporters/base.rb +87 -0
  38. data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
  39. data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
  40. data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
  41. data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
  42. data/lib/rspec_tracer/reporters/html/README.md +80 -0
  43. data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
  44. data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
  45. data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
  46. data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
  47. data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
  48. data/lib/rspec_tracer/reporters/html/package.json +29 -0
  49. data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
  50. data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
  51. data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
  52. data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
  53. data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
  54. data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
  55. data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
  56. data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
  57. data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
  58. data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
  59. data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
  60. data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
  61. data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
  62. data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
  63. data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
  64. data/lib/rspec_tracer/reporters/registry.rb +120 -0
  65. data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
  66. data/lib/rspec_tracer/rspec/README.md +73 -0
  67. data/lib/rspec_tracer/rspec/installation.rb +97 -0
  68. data/lib/rspec_tracer/rspec/metadata.rb +96 -0
  69. data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
  70. data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
  71. data/lib/rspec_tracer/rspec/runner_hook.rb +239 -0
  72. data/lib/rspec_tracer/source_file.rb +24 -7
  73. data/lib/rspec_tracer/storage/README.md +35 -0
  74. data/lib/rspec_tracer/storage/backend.rb +130 -0
  75. data/lib/rspec_tracer/storage/json_backend.rb +884 -0
  76. data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
  77. data/lib/rspec_tracer/storage/schema.rb +50 -0
  78. data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
  79. data/lib/rspec_tracer/storage/serializer/msgpack.rb +167 -0
  80. data/lib/rspec_tracer/storage/snapshot.rb +141 -0
  81. data/lib/rspec_tracer/storage/sqlite_backend.rb +693 -0
  82. data/lib/rspec_tracer/time_formatter.rb +37 -18
  83. data/lib/rspec_tracer/tracker/README.md +36 -0
  84. data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
  85. data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
  86. data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
  87. data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
  88. data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
  89. data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
  90. data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
  91. data/lib/rspec_tracer/tracker/filter.rb +127 -0
  92. data/lib/rspec_tracer/tracker/input.rb +99 -0
  93. data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
  94. data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
  95. data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
  96. data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
  97. data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
  98. data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
  99. data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
  100. data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
  101. data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
  102. data/lib/rspec_tracer/version.rb +4 -1
  103. data/lib/rspec_tracer.rb +231 -491
  104. metadata +94 -43
  105. data/lib/rspec_tracer/cache.rb +0 -207
  106. data/lib/rspec_tracer/coverage_merger.rb +0 -42
  107. data/lib/rspec_tracer/coverage_reporter.rb +0 -187
  108. data/lib/rspec_tracer/coverage_writer.rb +0 -58
  109. data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
  110. data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
  111. data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
  112. data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
  113. data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
  114. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
  115. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
  116. data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
  117. data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
  118. data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
  119. data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
  120. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
  121. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
  122. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
  123. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
  124. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
  125. data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
  126. data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
  127. data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
  128. data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
  129. data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
  130. data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
  131. data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
  132. data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
  133. data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
  134. data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
  135. data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
  136. data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
  137. data/lib/rspec_tracer/report_generator.rb +0 -158
  138. data/lib/rspec_tracer/report_merger.rb +0 -68
  139. data/lib/rspec_tracer/report_writer.rb +0 -141
  140. data/lib/rspec_tracer/reporter.rb +0 -204
  141. data/lib/rspec_tracer/rspec_reporter.rb +0 -41
  142. data/lib/rspec_tracer/rspec_runner.rb +0 -56
  143. data/lib/rspec_tracer/ruby_coverage.rb +0 -9
  144. data/lib/rspec_tracer/runner.rb +0 -278
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'snapshot'
4
+
5
+ module RSpecTracer
6
+ module Storage
7
+ # Lazy-reading view returned by `JsonBackend#load_graph`. Presents
8
+ # the same field surface as `Storage::Snapshot` but defers the disk
9
+ # read + deserialization for each field until the caller touches it.
10
+ #
11
+ # Rationale: on a large cache (100 MB+), eager-reading every file
12
+ # at setup dominates warm-run startup (issue #17). Engine
13
+ # at setup touches 10/15 fields; reporters never touch the previous
14
+ # snapshot; third-party tooling often reads one or two. The 3-5
15
+ # fields the Engine does NOT touch at setup (duplicate_examples,
16
+ # reverse_dependency, env_dependency; examples_coverage is deferred
17
+ # to finalize per the seed refactor) save their full disk+parse
18
+ # cost for every run.
19
+ #
20
+ # Duck-types `Snapshot`: every Struct member becomes a method here,
21
+ # `respond_to?` returns true for each, `to_h` materializes the
22
+ # whole thing. Callers doing `prev.send(field)` or `prev.field`
23
+ # work unchanged.
24
+ #
25
+ # Thread-safety: the memoization Hash is not guarded. Engine is
26
+ # single-threaded per run; parallel_tests workers each own their
27
+ # own LazySnapshot instance. If a future caller reads fields from
28
+ # multiple threads, wrap with a Monitor at construct time.
29
+ class LazySnapshot
30
+ LAZY_FIELDS = (Snapshot.members - %i[schema_version run_id]).freeze
31
+
32
+ attr_reader :schema_version, :run_id
33
+
34
+ def initialize(schema_version:, run_id:, reader:)
35
+ @schema_version = schema_version
36
+ @run_id = run_id
37
+ @reader = reader
38
+ @loaded = {}
39
+ end
40
+
41
+ LAZY_FIELDS.each do |field|
42
+ define_method(field) do
43
+ return @loaded[field] if @loaded.key?(field)
44
+
45
+ @loaded[field] = @reader.read(field)
46
+ end
47
+ end
48
+
49
+ # Hash view matching `Struct#to_h` so callers composing snapshots
50
+ # (merge pipelines, reporter helpers) get the familiar shape.
51
+ # Forces every field to materialize, so only call this when the
52
+ # full cache is genuinely required.
53
+ def to_h
54
+ Snapshot.members.to_h { |m| [m, public_send(m)] }
55
+ end
56
+
57
+ # Materialize a full eager Snapshot. Use when an API requires the
58
+ # Struct type (Merger input, backends that accept Snapshot on
59
+ # save). Reads every field.
60
+ def to_snapshot
61
+ Snapshot.new(**to_h)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ # Internal Storage — see {RSpecTracer} for the user-facing surface.
5
+ # @api private
6
+ module Storage
7
+ # Cache schema version and compatibility policy. 1.x shipped caches
8
+ # without any version stamp; 2.0's first migration is simply to
9
+ # start emitting a version and refuse to load anything else.
10
+ #
11
+ # Compatibility rule (from ARCHITECTURE.md):
12
+ # - `CURRENT` is written into every new cache manifest.
13
+ # - `SUPPORTED` is the set of versions this backend is willing
14
+ # to load. For 2.0 there is exactly one supported version.
15
+ # - On mismatch, `load_graph` returns nil after an `info` log
16
+ # line and the run proceeds cold. No in-place migrators.
17
+ #
18
+ # Future version bumps (schema_version 3, 4, ...) add entries to
19
+ # `SUPPORTED` only if the backend can load both shapes. If a
20
+ # change is breaking, `SUPPORTED` resets to `[CURRENT]` and the
21
+ # caller pays one cold run on upgrade - the deal 1.x users already
22
+ # expect for any rspec-tracer version bump.
23
+ module Schema
24
+ # 1.x caches were unstamped; schema_version 2 was the first
25
+ # versioned schema, and 2.0 bumped to 3 when `Snapshot.boot_set`
26
+ # landed. schema_version 4 reshaped the example-identity payload:
27
+ # `example_id` hashes the describe block's *description* (not
28
+ # RSpec's load-order-dependent class name) and excludes line
29
+ # numbers. schema_version 5 closes the remaining gap for unnamed
30
+ # examples (`it { }` / `specify { }` / `example { }`): their
31
+ # `example_id` now derives from an intra-group ordinal instead
32
+ # of RSpec's line-bearing `"example at <path>:<line>"` fallback,
33
+ # so a 4-stamped cache's unnamed-example ids no longer match.
34
+ # Each bump is breaking, so SUPPORTED stays `[CURRENT]` and the
35
+ # caller pays one cold run on upgrade.
36
+ CURRENT = 5
37
+ # Internal constant.
38
+ # @api private
39
+ SUPPORTED = [CURRENT].freeze
40
+
41
+ # True when the caller can load a cache stamped with `version`.
42
+ # nil (an unstamped 1.x cache) is explicitly unsupported -
43
+ # treating nil as "compatible" would defeat the whole point of
44
+ # the version field. The caller logs and falls back to cold run.
45
+ def self.supported?(version)
46
+ SUPPORTED.include?(version)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RSpecTracer
6
+ # Internal Storage — see {RSpecTracer} for the user-facing surface.
7
+ # @api private
8
+ module Storage
9
+ # Internal Serializer — see {RSpecTracer} for the user-facing surface.
10
+ # @api private
11
+ module Serializer
12
+ # Default serializer for JsonBackend. Produces pretty-printed
13
+ # JSON strings; reads tolerate binary-mode bytes by forcing
14
+ # UTF-8 on decode (preserves the fix for example titles with
15
+ # non-ASCII bytes on US-ASCII-defaulted filesystems).
16
+ #
17
+ # Class-level methods (not module_function) so mutant-rspec can
18
+ # observe mutations through the call path; see the mutation-
19
+ # friendly-modules memo.
20
+ class Json
21
+ # Internal helper for the tracer pipeline.
22
+ # @api private
23
+ def self.extension
24
+ 'json'
25
+ end
26
+
27
+ # Internal helper for the tracer pipeline.
28
+ # @api private
29
+ def self.encode(payload)
30
+ ::JSON.pretty_generate(payload)
31
+ end
32
+
33
+ # Internal helper for the tracer pipeline.
34
+ # @api private
35
+ def self.decode(bytes)
36
+ ::JSON.parse(bytes.dup.force_encoding('UTF-8'))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ module RSpecTracer
6
+ # Internal Storage — see {RSpecTracer} for the user-facing surface.
7
+ # @api private
8
+ module Storage
9
+ # Internal Serializer — see {RSpecTracer} for the user-facing surface.
10
+ # @api private
11
+ module Serializer
12
+ # Raised when the caller asks for :msgpack but the msgpack gem
13
+ # is not in the user's bundle. JsonBackend rescues at construct
14
+ # time + falls back to the Json serializer with a warn line,
15
+ # same optional-dep pattern the RedisBackend uses for the redis
16
+ # gem.
17
+ class MsgpackGemNotInstalled < StandardError; end
18
+
19
+ # MessagePack + zlib serializer. msgpack encode is ~2x smaller
20
+ # than pretty JSON on our representative dependency graphs;
21
+ # zlib deflate then buys another ~5x on path-repetitive
22
+ # payloads (dependency.json is the big one - the same path
23
+ # string appears once per example that touched it). Combined
24
+ # ratio matches the ~4-6x claim in remote_cache/archive.rb.
25
+ #
26
+ # Stdlib zlib avoids adding a second gem dep; msgpack itself is
27
+ # the single new dev-group gem. Users who want the backend
28
+ # add `gem 'msgpack'` to their own Gemfile per
29
+ # USER_FACING_SURFACE.md optional-dep convention.
30
+ #
31
+ # The require is lazy so pure-Ruby suites that stay on :json
32
+ # do not pay the msgpack load cost and so the LoadError path
33
+ # is exercisable in unit specs (hide_const-based).
34
+ #
35
+ # Type extensions: Ruby `Time` and `Symbol` values surface in
36
+ # example metadata (RSpec's `execution_result.started_at` is a
37
+ # Time; status values like `:passed` / `:failed` / `:flaky` are
38
+ # Symbols on the in-memory snapshot). The bare msgpack default
39
+ # registry has no `Time` packer (crashes
40
+ # `NoMethodError: undefined method 'to_msgpack'`) and silently
41
+ # coerces `Symbol` to `String` (lossy round-trip). Both
42
+ # behaviors broke the cache on the first run a user followed
43
+ # the 50 MiB warning's `:msgpack` recommendation. Registering
44
+ # `Factory` type extensions for `Time` (ID 0x00, 12-byte
45
+ # seconds+nanoseconds payload) and `Symbol` (ID 0x01, UTF-8
46
+ # string payload) gives lossless round-trip. The factory is
47
+ # memoized so the extension registration is one-shot per
48
+ # process.
49
+ class Msgpack
50
+ # Internal constant — `MessagePack::Factory#register_type`
51
+ # ID for Ruby `Time`. Codepoint chosen from the user-space
52
+ # range (0x00–0x7F per msgpack ext spec); collision-safe with
53
+ # the built-in timestamp ext (ID -1 / 0xFF) since IDs are
54
+ # disjoint.
55
+ # @api private
56
+ TIME_EXTENSION_TYPE = 0x00
57
+ # Internal constant — `MessagePack::Factory#register_type` ID
58
+ # for Ruby `Symbol`. See {TIME_EXTENSION_TYPE} rationale.
59
+ # @api private
60
+ SYMBOL_EXTENSION_TYPE = 0x01
61
+
62
+ # Internal helper for the tracer pipeline.
63
+ # @api private
64
+ def self.extension
65
+ 'msgpack.gz'
66
+ end
67
+
68
+ # Internal helper for the tracer pipeline.
69
+ # @api private
70
+ def self.encode(payload)
71
+ ::Zlib::Deflate.deflate(factory.pack(payload))
72
+ end
73
+
74
+ # Internal helper for the tracer pipeline.
75
+ # @api private
76
+ def self.decode(bytes)
77
+ factory.unpack(::Zlib::Inflate.inflate(bytes))
78
+ end
79
+
80
+ # Probe used by JsonBackend#initialize to decide whether to
81
+ # fall back to the Json serializer at construct time. True
82
+ # iff `require 'msgpack'` succeeds; false when the gem is
83
+ # missing. Idempotent without an explicit memo because Ruby's
84
+ # `require` short-circuits via `$LOADED_FEATURES` on repeat
85
+ # calls and `defined?(::MessagePack)` cheaply detects the
86
+ # post-load constant. The previous @msgpack_loaded ivar memo
87
+ # was load-bearing for mutation observability: once any prior
88
+ # test tripped the memo, mutations on the require line were
89
+ # observably equivalent.
90
+ def self.available?
91
+ ensure_available!
92
+ true
93
+ rescue MsgpackGemNotInstalled
94
+ false
95
+ end
96
+
97
+ class << self
98
+ private
99
+
100
+ # `MessagePack::Factory` with `Time` and `Symbol` type
101
+ # extensions registered. Memoized for the process lifetime;
102
+ # the factory's `register_type` calls are not idempotent
103
+ # (re-registering would double-register the codepoint), so
104
+ # one-shot construction is the simplest correctness
105
+ # contract.
106
+ def factory
107
+ return @factory if defined?(@factory) && @factory
108
+
109
+ ensure_available!
110
+ @factory = build_factory
111
+ end
112
+
113
+ # `Time` and `Symbol` packers / unpackers. Packers are
114
+ # called by `factory.pack` whenever a Ruby `Time` /
115
+ # `Symbol` appears at any depth in the payload (top-level
116
+ # value, nested Hash value, Array element). Unpackers are
117
+ # called by `factory.unpack` on the ext-typed bytes.
118
+ # Symbol pack/unpack maps to `to_s` / `to_sym` directly
119
+ # via `Symbol#to_proc`; Time is more involved so it gets
120
+ # named `pack_time` / `unpack_time` helpers below.
121
+ def build_factory
122
+ f = ::MessagePack::Factory.new
123
+ f.register_type(
124
+ TIME_EXTENSION_TYPE, ::Time,
125
+ packer: method(:pack_time),
126
+ unpacker: method(:unpack_time)
127
+ )
128
+ f.register_type(
129
+ SYMBOL_EXTENSION_TYPE, ::Symbol,
130
+ packer: :to_s.to_proc,
131
+ unpacker: :to_sym.to_proc
132
+ )
133
+ f
134
+ end
135
+
136
+ # Time encoding: 64-bit signed seconds (`tv_sec`) + 32-bit
137
+ # signed nanoseconds (`tv_nsec`), little-endian, 12 bytes
138
+ # total. Round-trip canonicalizes to UTC — rspec-tracer
139
+ # never re-uses the Time as a user-facing wall-clock value
140
+ # (cache entries are compared, not displayed), and UTC
141
+ # makes the on-disk bytes timezone-independent so a cache
142
+ # built in one tz reads back identically in another.
143
+ def pack_time(time)
144
+ [time.tv_sec, time.tv_nsec].pack('q<l<')
145
+ end
146
+
147
+ def unpack_time(data)
148
+ sec, nsec = data.unpack('q<l<')
149
+ ::Time.at(sec, nsec, :nanosecond).utc
150
+ end
151
+
152
+ # Internal method on the tracer pipeline.
153
+ # @api private
154
+ def ensure_available!
155
+ return if defined?(::MessagePack)
156
+
157
+ require 'msgpack'
158
+ rescue ::LoadError
159
+ raise MsgpackGemNotInstalled,
160
+ "msgpack gem is not installed; add `gem 'msgpack'` to your Gemfile " \
161
+ 'to use the :msgpack serializer.'
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module RSpecTracer
6
+ # Internal Storage — see {RSpecTracer} for the user-facing surface.
7
+ # @api private
8
+ module Storage
9
+ # Value object returned by `Backend#load_graph` and accepted by
10
+ # `Backend#save_graph`. Bundles every collection that 1.x's
11
+ # report_writer persists plus the schema_version + run_id envelope
12
+ # so a full cache can be reconstructed in one trip.
13
+ #
14
+ # The field layout mirrors 1.x's on-disk contract (see
15
+ # `JsonBackend::FILENAMES`); every field round-trips through
16
+ # `to_h` / `from_h` so the backend can serialize without reaching
17
+ # into struct internals.
18
+ #
19
+ # `examples_coverage` may be `nil` when a caller explicitly loads
20
+ # the cheap header only. The default load is eager - `nil` vs `{}`
21
+ # distinguishes "not yet loaded" from "loaded and empty."
22
+ #
23
+ # Methods are defined on the reopened class body (not inside the
24
+ # Struct.new block) so mutant can introspect them - same pattern
25
+ # as Tracker::Input.
26
+ #
27
+ # `boot_set` - Hash[relative_path => sha256_hex] of every project
28
+ # file loaded before any example runs (spec_helper requires, gem
29
+ # boot, eager autoload). Backs the constants-blind-spot fix: the
30
+ # engine compares this against the previous run's boot_set and ORs
31
+ # any mismatch with WholeSuiteInvalidators when computing the
32
+ # whole_suite_invalidated bool. Per-example loaded-set attribution
33
+ # folds into `dependency` via the existing graph registration path
34
+ # - no separate field.
35
+ #
36
+ # `wsi_snapshot` - Hash[watch_name => sha256_hex] produced by
37
+ # `WholeSuiteInvalidators#digest_snapshot`. Without it, a warm run
38
+ # can't tell whether Gemfile.lock / .ruby-version / .rspec-tracer
39
+ # (or the tracer gem identity) changed since the previous run, and
40
+ # the engine falls back to "first run = invalidate everything" on
41
+ # every warm run. The field is optional in the JSON layout so older
42
+ # caches continue to load (missing wsi.json coerces to `{}`, which
43
+ # compares unequal and triggers one cold re-run - safe fallback,
44
+ # same cost as any other cache miss).
45
+ #
46
+ # `env_snapshot` - Hash[env_name => md5_hex] produced by
47
+ # `Tracker::EnvSnapshot#digest_snapshot`. Covers env-var values
48
+ # declared via the per-example `tracks: { env: ... }` DSL.
49
+ # Without it, a warm run can't tell whether an env-gated example
50
+ # needs to re-run when the env changes. Same optional-in-JSON
51
+ # treatment as wsi_snapshot: missing file coerces to `{}`, no
52
+ # schema_version bump, one cold re-run on upgrade.
53
+ #
54
+ # `env_dependency` - Hash[example_id => Array<env_name>] capturing
55
+ # which env keys each tracked example declared. The per-run
56
+ # `env_snapshot` stores the digest of each key; this map stores
57
+ # the example-to-key attribution that the reporter layer needs to
58
+ # render "which env vars does this example depend on." Without it,
59
+ # reports can't surface env dependencies - Engine's per-run
60
+ # `@tracks_env` map would otherwise be lost at finalize.
61
+ # Same optional-in-JSON treatment as wsi_snapshot / env_snapshot:
62
+ # missing file coerces to `{}`, no schema_version bump.
63
+ #
64
+ # `cache_hit_reason` is a SUITE-LEVEL aggregate. It maps
65
+ # `reason_string => count` (e.g. `{"Files changed" => 12,
66
+ # "No cache" => 5}`) and surfaces "why did each non-skipped
67
+ # example run." JsonBackend persists it as `cache_hit_reason.json`
68
+ # under the per-run dir (same shape as wsi_snapshot / env_snapshot:
69
+ # one file per field; missing file coerces to `{}`, no
70
+ # schema_version bump). SqliteBackend does not persist it (would
71
+ # require a meta-table column / schema bump); SqliteBackend users
72
+ # see the field as `{}` on read until a future enhancement
73
+ # extends the meta table.
74
+ #
75
+ # `filtered_examples` is the PER-EXAMPLE source-of-truth that
76
+ # backs `cache_hit_reason`: a Hash[example_id => reason_string]
77
+ # ("ex_abc" => "Failed previously"). The engine writes this at
78
+ # finalize; `cache_hit_reason` is the values-tally. Persisting
79
+ # both lets the parallel-tests merge collapse per-worker
80
+ # duplicates by id (every worker computes the same hash because
81
+ # the filter walks the global previous-run snapshot) and re-tally
82
+ # at merge time, instead of sum-merging identical per-worker
83
+ # tallies and inflating counts N-fold. Same optional-in-JSON
84
+ # treatment as cache_hit_reason: missing file coerces to `{}`,
85
+ # no schema_version bump, JSON-backend-only surface.
86
+ Snapshot = Struct.new(
87
+ :schema_version,
88
+ :run_id,
89
+ :all_examples,
90
+ :duplicate_examples,
91
+ :interrupted_examples,
92
+ :flaky_examples,
93
+ :failed_examples,
94
+ :pending_examples,
95
+ :skipped_examples,
96
+ :all_files,
97
+ :dependency,
98
+ :reverse_dependency,
99
+ :examples_coverage,
100
+ :boot_set,
101
+ :wsi_snapshot,
102
+ :env_snapshot,
103
+ :env_dependency,
104
+ :cache_hit_reason,
105
+ :filtered_examples,
106
+ keyword_init: true
107
+ )
108
+
109
+ # Internal Snapshot — see {RSpecTracer} for the user-facing surface.
110
+ # @api private
111
+ class Snapshot
112
+ # Defaults every collection to its 1.x starting shape: Hash for
113
+ # keyed collections, Set for example-id lists. Keeps spec
114
+ # construction terse and prevents accidental nil-deref when a
115
+ # save is composed incrementally.
116
+ def self.empty(schema_version:, run_id:)
117
+ new(
118
+ schema_version: schema_version,
119
+ run_id: run_id,
120
+ all_examples: {},
121
+ duplicate_examples: {},
122
+ interrupted_examples: Set.new,
123
+ flaky_examples: Set.new,
124
+ failed_examples: Set.new,
125
+ pending_examples: Set.new,
126
+ skipped_examples: Set.new,
127
+ all_files: {},
128
+ dependency: {},
129
+ reverse_dependency: {},
130
+ examples_coverage: {},
131
+ boot_set: {},
132
+ wsi_snapshot: {},
133
+ env_snapshot: {},
134
+ env_dependency: {},
135
+ cache_hit_reason: {},
136
+ filtered_examples: {}
137
+ )
138
+ end
139
+ end
140
+ end
141
+ end