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 +7 -0
- data/LICENSE +21 -0
- data/README.md +339 -0
- data/ext/fast_cov/extconf.rb +12 -0
- data/ext/fast_cov/fast_cov.c +615 -0
- data/ext/fast_cov/fast_cov.h +32 -0
- data/ext/fast_cov/fast_cov_utils.c +104 -0
- data/lib/fast_cov/benchmark/runner.rb +147 -0
- data/lib/fast_cov/benchmark/scenarios.rb +103 -0
- data/lib/fast_cov/compiler.rb +56 -0
- data/lib/fast_cov/configuration.rb +27 -0
- data/lib/fast_cov/constant_extractor.rb +53 -0
- data/lib/fast_cov/dev.rb +22 -0
- data/lib/fast_cov/trackers/abstract_tracker.rb +73 -0
- data/lib/fast_cov/trackers/coverage_tracker.rb +28 -0
- data/lib/fast_cov/trackers/factory_bot_tracker.rb +52 -0
- data/lib/fast_cov/trackers/file_tracker.rb +48 -0
- data/lib/fast_cov/version.rb +5 -0
- data/lib/fast_cov.rb +62 -0
- metadata +117 -0
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}")
|