fast_cov 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 255f8e8573c52c46aa3084f2f5444b13fd142e80106ccc34415de8479471fd78
4
+ data.tar.gz: ee144a0c299aeb4f9ed6e9be81650fa228c529fa6f6ee2be0cd0a38ed67a2810
5
+ SHA512:
6
+ metadata.gz: abfe0d5981fabc15fe062fb0d203424b85c6716890da0db6781ca9e469d536871fec86f8120edfac0ec4ace7a75a8bf43befac6f70d365b571bf532257eb8aea
7
+ data.tar.gz: d7b53cc4afa95237f59ebaebe6c115679853d27c4378a66bc315ff8b71897369037e4ec87cc3d436d2975f1005e023fb5d4dc06c6e01eb919670bf46ddde805c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gusto, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # FastCov
2
+
3
+ A high-performance native C extension for tracking which Ruby source files are executed during test runs. Built for test impact analysis -- run only the tests affected by your code changes.
4
+
5
+ FastCov hooks directly into the Ruby VM's event system, avoiding the overhead of Ruby's built-in `Coverage` module. The result is file-level coverage tracking with minimal performance impact.
6
+
7
+ ## Requirements
8
+
9
+ - Ruby >= 3.4.0 (MRI only)
10
+ - macOS or Linux
11
+
12
+ ## Installation
13
+
14
+ Add to your Gemfile:
15
+
16
+ ```ruby
17
+ gem "fast_cov"
18
+ ```
19
+
20
+ Then:
21
+
22
+ ```sh
23
+ bundle install
24
+ ```
25
+
26
+ The C extension compiles automatically during gem installation.
27
+
28
+ ## Quick start
29
+
30
+ ```ruby
31
+ require "fast_cov"
32
+
33
+ FastCov.configure do |config|
34
+ config.root = File.expand_path("app")
35
+ config.use FastCov::CoverageTracker
36
+ config.use FastCov::FileTracker
37
+ end
38
+
39
+ result = FastCov.start do
40
+ # ... run a test ...
41
+ end
42
+
43
+ # => { "models/user.rb" => true, "config.yml" => true, ... }
44
+ ```
45
+
46
+ `stop` returns a hash where each key is the path (relative to `root`) of a file that was touched during the coverage window.
47
+
48
+ ## Configuration
49
+
50
+ Call `FastCov.configure` before using `start`/`stop`. The block yields a `Configuration` object:
51
+
52
+ ```ruby
53
+ FastCov.configure do |config|
54
+ config.root = Rails.root.to_s
55
+ config.ignored_path = Rails.root.join("vendor").to_s
56
+ config.threads = true
57
+
58
+ config.use FastCov::CoverageTracker
59
+ config.use FastCov::FileTracker
60
+ end
61
+ ```
62
+
63
+ ### Config options
64
+
65
+ | Option | Type | Default | Description |
66
+ |---|---|---|---|
67
+ | `root` | String | `Dir.pwd` | Absolute path to the project root. Only files under this path are tracked. |
68
+ | `ignored_path` | String | `nil` | Path prefix to exclude (e.g., vendor/bundle). |
69
+ | `threads` | Boolean | `true` | `true` tracks all threads. `false` tracks only the thread that called `start`. |
70
+
71
+ ### Registering trackers
72
+
73
+ Trackers are registered with `config.use`. Each tracker receives the config object and any options you pass:
74
+
75
+ ```ruby
76
+ config.use FastCov::CoverageTracker
77
+ config.use FastCov::CoverageTracker, constant_references: false
78
+ config.use FastCov::FileTracker, ignored_path: "/custom/ignore"
79
+ ```
80
+
81
+ ## Singleton API
82
+
83
+ ```ruby
84
+ FastCov.configure { |c| ... } # Configure and install trackers
85
+ FastCov.start # Start all trackers. Returns FastCov.
86
+ FastCov.stop # Stop all trackers. Returns merged results hash.
87
+ FastCov.start { ... } # Block form: start, yield, stop. Returns results.
88
+ FastCov.configured? # true after configure, false after reset.
89
+ FastCov.reset # Clear configuration and trackers.
90
+ ```
91
+
92
+ ### RSpec integration
93
+
94
+ ```ruby
95
+ # spec/support/fast_cov.rb
96
+ FastCov.configure do |config|
97
+ config.root = Rails.root.to_s
98
+ config.use FastCov::CoverageTracker
99
+ config.use FastCov::FileTracker
100
+ end
101
+
102
+ RSpec.configure do |config|
103
+ config.around(:each) do |example|
104
+ result = FastCov.start { example.run }
105
+ # result is a hash of impacted file paths
106
+ end
107
+ end
108
+ ```
109
+
110
+ ## Trackers
111
+
112
+ ### CoverageTracker
113
+
114
+ Wraps the native C extension. Handles line event tracking, allocation tracing, and constant reference resolution.
115
+
116
+ ```ruby
117
+ config.use FastCov::CoverageTracker
118
+ ```
119
+
120
+ #### Options
121
+
122
+ | Option | Type | Default | Description |
123
+ |---|---|---|---|
124
+ | `root` | String | `config.root` | Override the root path for this tracker. |
125
+ | `ignored_path` | String | `config.ignored_path` | Override the ignored path for this tracker. |
126
+ | `threads` | Boolean | `config.threads` | Override the threading mode for this tracker. |
127
+ | `allocations` | Boolean | `true` | Track object allocations and resolve class hierarchies to source files. |
128
+ | `constant_references` | Boolean | `true` | Parse source with Prism for constant references and resolve them to defining files. |
129
+
130
+ #### What it tracks
131
+
132
+ **Line events** -- hooks `RUBY_EVENT_LINE` to record which files execute. Uses pointer caching (`rb_sourcefile()` returns stable pointers) to skip redundant file checks with a single integer comparison.
133
+
134
+ **Allocation tracing** (`allocations: true`) -- hooks `RUBY_INTERNAL_EVENT_NEWOBJ` to capture `T_OBJECT` and `T_STRUCT` allocations. At stop time, walks each instantiated class's ancestor chain and resolves every ancestor to its source file. This catches empty models, structs, and Data objects that line events alone would miss.
135
+
136
+ **Constant reference resolution** (`constant_references: true`) -- at stop time, parses tracked files with Prism and walks the AST for `ConstantPathNode` and `ConstantReadNode` to extract constant references, then resolves each constant to its defining file via `Object.const_source_location`. Resolution is transitive (up to 10 rounds) and cached with MD5 digests for invalidation.
137
+
138
+ #### Disabling expensive features
139
+
140
+ For maximum speed when you only need line-level file tracking:
141
+
142
+ ```ruby
143
+ config.use FastCov::CoverageTracker, allocations: false, constant_references: false
144
+ ```
145
+
146
+ This disables the NEWOBJ hook (no per-allocation overhead) and skips AST parsing at stop time.
147
+
148
+ ### FileTracker
149
+
150
+ Tracks files read from disk during coverage -- JSON, YAML, ERB templates, or any file accessed via `File.read` or `File.open`.
151
+
152
+ ```ruby
153
+ config.use FastCov::FileTracker
154
+ ```
155
+
156
+ #### Options
157
+
158
+ | Option | Type | Default | Description |
159
+ |---|---|---|---|
160
+ | `root` | String | `config.root` | Override the root path for this tracker. |
161
+ | `ignored_path` | String | `config.ignored_path` | Override the ignored path for this tracker. |
162
+ | `threads` | Boolean | `config.threads` | Override the threading mode for this tracker. |
163
+
164
+ #### How it works
165
+
166
+ Prepends a module on `File.singleton_class` to intercept `File.read` and `File.open` (read-mode only). When a file within the root is read during coverage, its path is recorded. Write operations (`"w"`, `"a"`, etc.) are ignored.
167
+
168
+ This catches `YAML.load_file`, `JSON.parse(File.read(...))`, `CSV.read`, ERB template loading, and any other pattern that goes through `File.read` or `File.open`.
169
+
170
+ ### FactoryBotTracker
171
+
172
+ Tracks FactoryBot factory definition files when factories are used during tests. Factory files are typically loaded at boot time before coverage starts, so this tracker intercepts `FactoryBot.factories.find` to record the source file where each factory was defined.
173
+
174
+ ```ruby
175
+ config.use FastCov::FactoryBotTracker
176
+ ```
177
+
178
+ **Requires:** The `factory_bot` gem must be installed. Raises `LoadError` if FactoryBot is not defined.
179
+
180
+ #### Options
181
+
182
+ | Option | Type | Default | Description |
183
+ |---|---|---|---|
184
+ | `root` | String | `config.root` | Override the root path for this tracker. |
185
+ | `ignored_path` | String | `config.ignored_path` | Override the ignored path for this tracker. |
186
+ | `threads` | Boolean | `config.threads` | Override the threading mode for this tracker. |
187
+
188
+ #### How it works
189
+
190
+ Prepends a module on `FactoryBot.factories.singleton_class` to intercept the `find` method (called by `create`, `build`, etc.). When a factory is used, the tracker walks its declaration blocks and extracts `source_location` from each proc to find the factory definition file.
191
+
192
+ ## Writing custom trackers
193
+
194
+ There are two approaches to writing custom trackers: from scratch (minimal interface) or inheriting from `AbstractTracker` (batteries included).
195
+
196
+ ### Option 1: From scratch
197
+
198
+ Any object that responds to `start` and `stop` can be a tracker. This is the minimal interface:
199
+
200
+ ```ruby
201
+ class MyTracker
202
+ def initialize(config, **options)
203
+ @config = config
204
+ @options = options
205
+ @files = {}
206
+ end
207
+
208
+ def install
209
+ # Optional: one-time setup (called during configure)
210
+ # Good place to patch classes, set up hooks, etc.
211
+ end
212
+
213
+ def start
214
+ @files = {}
215
+ # Begin tracking
216
+ end
217
+
218
+ def stop
219
+ # Stop tracking and return results
220
+ # Paths should be absolute; FastCov will relativize them to config.root
221
+ @files
222
+ end
223
+ end
224
+ ```
225
+
226
+ ### Option 2: Inherit from AbstractTracker
227
+
228
+ `AbstractTracker` provides common functionality out of the box:
229
+
230
+ - **Path filtering** — Only records files within `root`, excludes `ignored_path`
231
+ - **Thread-aware recording** — Respects the `threads` option
232
+ - **Lifecycle management** — Handles `@files` hash and `active` class attribute
233
+
234
+ ```ruby
235
+ class MyTracker < FastCov::AbstractTracker
236
+ def install
237
+ # Patch the class/module you want to track
238
+ SomeClass.singleton_class.prepend(MyPatch)
239
+ end
240
+
241
+ module MyPatch
242
+ def some_method(...)
243
+ # Record the file when this method is called
244
+ # Uses inherited class method - no need to check .active
245
+ MyTracker.record(some_file_path)
246
+ super
247
+ end
248
+ end
249
+ end
250
+ ```
251
+
252
+ #### AbstractTracker hooks
253
+
254
+ Override these methods as needed:
255
+
256
+ | Method | When called | Purpose |
257
+ |---|---|---|
258
+ | `install` | Once during `configure` | Set up patches, hooks, instrumentation |
259
+ | `on_start` | At the beginning of `start` | Initialize tracker-specific state |
260
+ | `on_stop` | At the beginning of `stop` | Clean up tracker-specific state |
261
+ | `on_record(path)` | When `record(path)` is called | Return `true` to record, `false` to skip |
262
+
263
+ The base `record(path)` method handles path filtering and thread checks before calling `on_record`.
264
+
265
+ #### Full example: tracking ActiveRecord queries
266
+
267
+ ```ruby
268
+ class QueryTracker < FastCov::AbstractTracker
269
+ def install
270
+ return unless defined?(ActiveSupport::Notifications)
271
+
272
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
273
+ # Extract the caller location from the backtrace
274
+ caller_locations(1, 20).each do |loc|
275
+ path = loc.absolute_path
276
+ next unless path
277
+
278
+ # Uses inherited class method - safely no-ops if tracker isn't active
279
+ QueryTracker.record(path)
280
+ break
281
+ end
282
+ end
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### Tracker lifecycle
288
+
289
+ 1. `initialize(config, **options)` — Called when registered via `config.use`
290
+ 2. `install` — Called once after all trackers are registered
291
+ 3. `start` — Called on `FastCov.start` (in registration order)
292
+ 4. `stop` — Called on `FastCov.stop` (in reverse order), must return `{ path => true }`
293
+
294
+ Results from all trackers are merged, with later trackers overwriting earlier ones for duplicate keys.
295
+
296
+ ## Cache
297
+
298
+ FastCov caches constant reference resolution results in memory so files only need parsing once per process. The cache is process-level, content-addressed (MD5 digests), and populated automatically during `stop`.
299
+
300
+ ```ruby
301
+ FastCov::Cache.data # the raw cache hash
302
+ FastCov::Cache.clear # empty the cache
303
+ FastCov::Cache.data = {} # replace cache contents
304
+ ```
305
+
306
+ ## Local development with path: gems
307
+
308
+ When developing FastCov alongside a consuming project, use the compile entrypoint to auto-compile the C extension:
309
+
310
+ ```ruby
311
+ # Gemfile
312
+ gem "fast_cov", path: "../fast_cov", require: "fast_cov/dev"
313
+ ```
314
+
315
+ This compiles on first use and detects source changes for recompilation.
316
+
317
+ ## Development
318
+
319
+ ```sh
320
+ git clone <repo>
321
+ cd fast_cov
322
+ bundle install
323
+ bundle exec rake compile # compile the C extension
324
+ bundle exec rake spec # run tests (compiles first)
325
+ ```
326
+
327
+ ### Benchmarking
328
+
329
+ ```sh
330
+ bin/benchmark --baseline # save current performance as baseline
331
+ # ... make changes ...
332
+ bin/benchmark # compare against baseline
333
+ ```
334
+
335
+ Override iteration count: `ITERATIONS=5000 bin/benchmark`
336
+
337
+ ## License
338
+
339
+ MIT
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_ENGINE != "ruby" || Gem.win_platform?
4
+ warn("WARN: Skipping build of fast_cov native extension (unsupported platform).")
5
+ File.write("Makefile", "all install clean: # dummy makefile\n")
6
+ exit
7
+ end
8
+
9
+ require "mkmf"
10
+
11
+ # Tag with Ruby version + platform so multiple versions coexist in lib/fast_cov/.
12
+ create_makefile("fast_cov.#{RUBY_VERSION}_#{RUBY_PLATFORM}")