fast_cov 0.1.6 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02e7d0d8bdf204b16fc1929fa0728147942db6719ea21324d4c0603554d2430f
4
- data.tar.gz: fc145576ae62e8c4ea0f3b289450bb2e53a234298c1099f41db2712613d6998f
3
+ metadata.gz: c1319e05ab0d7a740626e94ed880dacb09b980dfc0ededc6b1c0259383fb0667
4
+ data.tar.gz: dd7af260d917264ab2f30affbc3dd2dfa44f5ddba03bc8d1f8dd222e29a9817e
5
5
  SHA512:
6
- metadata.gz: 1f046ea86225a528b7b3897c6cb57859204cf07c934f555c96166e5540cef844a1f84782cd1cae6b32d1bacf3c6e0aaa0b06a20b82574bd0f4bbb8d7b4286f02
7
- data.tar.gz: 1427db72e9b1ccbf3e623f09bed8e8db16382cc0f3af0d614e9c2d19ed41a64358c87e1aa27d8a64f80d7a9815d1f8aa521e7d15845c687d7efe31efe7a48b4b
6
+ metadata.gz: 6b594da4510ba6bb43767030b4d25e3f7787e22923b6e76b48616f2a085c030d22c1d870976570f7ac74fe921fe2c1eabbeeb16471ec8a533588f9d5b9974fef
7
+ data.tar.gz: 98f1b25e2565a56af55e472096a4c3159110b27117b99682cd031888083ec68d5aef8788aeff7a2538af9a5287f588653f8717ee30160e1126f127a0316191ea
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FastCov
2
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.
3
+ A high-performance native C extension for tracking which Ruby source files are executed during test runs. Built for test impact analysis.
4
4
 
5
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
6
 
@@ -23,229 +23,128 @@ Then:
23
23
  bundle install
24
24
  ```
25
25
 
26
- The C extension compiles automatically during gem installation.
27
-
28
26
  ## Quick start
29
27
 
30
28
  ```ruby
31
29
  require "fast_cov"
32
30
 
33
- FastCov.configure do |config|
34
- config.root = File.expand_path("app")
35
- config.use FastCov::CoverageTracker
36
- config.use FastCov::FileTracker
37
- end
31
+ coverage = FastCov::CoverageMap.new
32
+ coverage.root = File.expand_path("app")
33
+ coverage.use(FastCov::FileTracker)
38
34
 
39
- result = FastCov.start do
35
+ result = coverage.start do
40
36
  # ... run a test ...
41
37
  end
42
38
 
43
- # => { "models/user.rb" => true, "config.yml" => true, ... }
39
+ # => #<Set: {"models/user.rb", "config/settings.yml"}>
44
40
  ```
45
41
 
46
- `stop` returns a hash where each key is the path (relative to `root`) of a file that was touched during the coverage window.
42
+ `CoverageMap#stop` returns a `Set` of paths relative to `root`.
47
43
 
48
- ## Configuration
44
+ ## CoverageMap
49
45
 
50
- Call `FastCov.configure` before using `start`/`stop`. The block yields a `Configuration` object:
46
+ `FastCov::CoverageMap` is the primary API.
51
47
 
52
48
  ```ruby
53
- FastCov.configure do |config|
54
- config.root = Rails.root
55
- config.ignored_path = Rails.root.join("vendor")
56
- config.threads = true
49
+ coverage = FastCov::CoverageMap.new
50
+ coverage.root = Rails.root.to_s
51
+ coverage.threads = true
52
+ coverage.ignored_paths = Rails.root.join("vendor")
57
53
 
58
- config.use FastCov::CoverageTracker
59
- config.use FastCov::FileTracker
60
- end
54
+ coverage.use(FastCov::FileTracker)
55
+ coverage.use(FastCov::FactoryBotTracker)
61
56
  ```
62
57
 
63
- ### Config options
58
+ ### Options
64
59
 
65
60
  | Option | Type | Default | Description |
66
61
  |---|---|---|---|
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). |
62
+ | `root` | String | `Dir.pwd` | Absolute project root. Returned paths are relativized to this root. |
69
63
  | `threads` | Boolean | `true` | `true` tracks all threads. `false` tracks only the thread that called `start`. |
64
+ | `ignored_paths` | String, Pathname, or Array | `[]` | Paths under `root` to exclude from tracking. Single values are wrapped into an array, and relative entries are resolved against `root` when coverage starts. |
70
65
 
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
66
+ ### Lifecycle
82
67
 
83
68
  ```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.
69
+ coverage.start # starts tracking and returns the CoverageMap
70
+ coverage.stop # stops tracking and returns a Set
71
+ coverage.start { ... } # block form: start, yield, stop
90
72
  ```
91
73
 
92
- ### RSpec integration
93
-
94
- ```ruby
95
- # spec/support/fast_cov.rb
96
- FastCov.configure do |config|
97
- config.root = Rails.root
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
- ```
74
+ Native line coverage is always enabled. Extra trackers registered with `use` are additive.
109
75
 
110
76
  ## Trackers
111
77
 
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 by filename for the lifetime of the process.
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
78
  ### FileTracker
149
79
 
150
- Tracks files read from disk during coverage -- JSON, YAML, ERB templates, or any file accessed via `File.read` or `File.open`.
80
+ Tracks files read from disk during coverage, including JSON, YAML, ERB templates, and any file accessed via `File.read` or read-mode `File.open`.
151
81
 
152
82
  ```ruby
153
- config.use FastCov::FileTracker
83
+ coverage.use(FastCov::FileTracker)
154
84
  ```
155
85
 
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
86
  ### FactoryBotTracker
171
87
 
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.
88
+ Tracks FactoryBot factory definition files when factories are used. This is useful because factory files are often loaded before coverage starts.
173
89
 
174
90
  ```ruby
175
- config.use FastCov::FactoryBotTracker
91
+ coverage.use(FastCov::FactoryBotTracker)
176
92
  ```
177
93
 
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
94
  ### ConstGetTracker
193
95
 
194
- Tracks constants looked up dynamically via `Module#const_get`. This catches dynamic constant lookups that static analysis (Prism) would miss.
96
+ Tracks constants looked up dynamically via `Module#const_get`.
195
97
 
196
98
  ```ruby
197
- config.use FastCov::ConstGetTracker
99
+ coverage.use(FastCov::ConstGetTracker)
198
100
  ```
199
101
 
200
- #### What it catches
102
+ This catches patterns such as:
201
103
 
202
104
  - `Object.const_get("Foo::Bar")`
203
- - Rails' `"UserMailer".constantize` (uses `const_get` under the hood)
204
- - Any metaprogramming that looks up constants by string name
105
+ - Rails `"UserMailer".constantize`
106
+ - metaprogramming that resolves constants from strings
205
107
 
206
- **Note:** This does NOT catch direct constant references like `Foo::Bar` in source code -- those compile to `opt_getconstant_path` bytecode and bypass `const_get`. Use `CoverageTracker` with `constant_references: true` for static analysis of literal constant references.
108
+ It does not catch direct constant references such as `Foo::Bar` in source code.
207
109
 
208
- #### Options
110
+ ## Low-level native coverage
209
111
 
210
- | Option | Type | Default | Description |
211
- |---|---|---|---|
212
- | `root` | String | `config.root` | Override the root path for this tracker. |
213
- | `ignored_path` | String | `config.ignored_path` | Override the ignored path for this tracker. |
214
- | `threads` | Boolean | `config.threads` | Override the threading mode for this tracker. |
112
+ `FastCov::Coverage` is still available as a low-level primitive:
215
113
 
216
- #### How it works
114
+ ```ruby
115
+ cov = FastCov::Coverage.new(
116
+ root: "/repo/app",
117
+ ignored_paths: ["/repo/app/vendor"],
118
+ threads: true
119
+ )
120
+ ```
217
121
 
218
- Prepends a module on `Module` to intercept `const_get` calls. When a constant is looked up, the tracker calls `const_source_location` to find where the constant was defined and records that file.
122
+ This API is mainly useful for internal use and low-level tests. `CoverageMap` is the intended public orchestration API.
219
123
 
220
124
  ## Writing custom trackers
221
125
 
222
- There are two approaches to writing custom trackers: from scratch (minimal interface) or inheriting from `AbstractTracker` (batteries included).
126
+ There are two approaches: a minimal custom tracker, or inheriting from `AbstractTracker`.
223
127
 
224
128
  ### Option 1: From scratch
225
129
 
226
- Any object that responds to `start` and `stop` can be a tracker. This is the minimal interface:
130
+ Any object that responds to `start` and `stop` can be used.
227
131
 
228
132
  ```ruby
229
133
  class MyTracker
230
- def initialize(config, **options)
231
- @config = config
134
+ def initialize(coverage_map, **options)
135
+ @coverage_map = coverage_map
232
136
  @options = options
233
- @files = {}
137
+ @files = Set.new
234
138
  end
235
139
 
236
140
  def install
237
- # Optional: one-time setup (called during configure)
238
- # Good place to patch classes, set up hooks, etc.
239
141
  end
240
142
 
241
143
  def start
242
- @files = {}
243
- # Begin tracking
144
+ @files = Set.new
244
145
  end
245
146
 
246
147
  def stop
247
- # Stop tracking and return results
248
- # Paths should be absolute; FastCov will relativize them to config.root
249
148
  @files
250
149
  end
251
150
  end
@@ -253,23 +152,21 @@ end
253
152
 
254
153
  ### Option 2: Inherit from AbstractTracker
255
154
 
256
- `AbstractTracker` provides common functionality out of the box:
155
+ `AbstractTracker` provides:
257
156
 
258
- - **Path filtering** Only records files within `root`, excludes `ignored_path`
259
- - **Thread-aware recording** — Respects the `threads` option
260
- - **Lifecycle management** — Handles `@files` hash and `active` class attribute
157
+ - path filtering through the owning `CoverageMap`
158
+ - thread-aware recording
159
+ - lifecycle management
160
+ - class-level `record` dispatch for patched hooks
261
161
 
262
162
  ```ruby
263
163
  class MyTracker < FastCov::AbstractTracker
264
164
  def install
265
- # Patch the class/module you want to track
266
165
  SomeClass.singleton_class.prepend(MyPatch)
267
166
  end
268
167
 
269
168
  module MyPatch
270
169
  def some_method(...)
271
- # Record the file when this method is called
272
- # Uses inherited class method - no need to check .active
273
170
  MyTracker.record(some_file_path)
274
171
  super
275
172
  end
@@ -277,91 +174,29 @@ class MyTracker < FastCov::AbstractTracker
277
174
  end
278
175
  ```
279
176
 
280
- #### AbstractTracker hooks
281
-
282
- Override these methods as needed:
283
-
284
- | Method | When called | Purpose |
285
- |---|---|---|
286
- | `install` | Once during `configure` | Set up patches, hooks, instrumentation |
287
- | `on_start` | At the beginning of `start` | Initialize tracker-specific state |
288
- | `on_stop` | At the beginning of `stop` | Clean up tracker-specific state |
289
- | `on_record(path)` | When `record(path)` is called | Return `true` to record, `false` to skip |
177
+ #### Hooks
290
178
 
291
- The base `record(path)` method handles path filtering and thread checks before calling `on_record`.
292
-
293
- #### Full example: tracking ActiveRecord queries
294
-
295
- ```ruby
296
- class QueryTracker < FastCov::AbstractTracker
297
- def install
298
- return unless defined?(ActiveSupport::Notifications)
299
-
300
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
301
- # Extract the caller location from the backtrace
302
- caller_locations(1, 20).each do |loc|
303
- path = loc.absolute_path
304
- next unless path
305
-
306
- # Uses inherited class method - safely no-ops if tracker isn't active
307
- QueryTracker.record(path)
308
- break
309
- end
310
- end
311
- end
312
- end
313
- ```
314
-
315
- ### Tracker lifecycle
316
-
317
- 1. `initialize(config, **options)` — Called when registered via `config.use`
318
- 2. `install` — Called once after all trackers are registered
319
- 3. `start` — Called on `FastCov.start` (in registration order)
320
- 4. `stop` — Called on `FastCov.stop` (in reverse order), must return `{ path => true }`
321
-
322
- Results from all trackers are merged, with later trackers overwriting earlier ones for duplicate keys.
323
-
324
- ## Cache
325
-
326
- FastCov caches constant reference resolution results in memory so files only need parsing once per process. The cache is process-level, keyed by filename, and populated automatically during `stop`.
327
-
328
- ```ruby
329
- FastCov::Cache.data # the raw cache hash
330
- FastCov::Cache.clear # empty the cache
331
- FastCov::Cache.data = {} # replace cache contents
332
- ```
179
+ | Method | When called |
180
+ |---|---|
181
+ | `install` | Once when the tracker is registered with `CoverageMap#use` |
182
+ | `on_start` | At the beginning of `start` |
183
+ | `on_stop` | At the beginning of `stop` |
184
+ | `on_record(path)` | Before a path is added to the result set |
333
185
 
334
186
  ## Local development with path: gems
335
187
 
336
188
  When developing FastCov alongside a consuming project, use the compile entrypoint to auto-compile the C extension:
337
189
 
338
190
  ```ruby
339
- # Gemfile
340
191
  gem "fast_cov", path: "../fast_cov", require: "fast_cov/dev"
341
192
  ```
342
193
 
343
- This compiles on first use and detects source changes for recompilation.
344
-
345
194
  ## Development
346
195
 
347
196
  ```sh
348
197
  git clone <repo>
349
198
  cd fast_cov
350
199
  bundle install
351
- bundle exec rake compile # compile the C extension
352
- bundle exec rake spec # run tests (compiles first)
200
+ bundle exec rake compile
201
+ bundle exec rspec --fail-fast
353
202
  ```
354
-
355
- ### Benchmarking
356
-
357
- ```sh
358
- bin/benchmark --baseline # save current performance as baseline
359
- # ... make changes ...
360
- bin/benchmark # compare against baseline
361
- ```
362
-
363
- Override iteration count: `ITERATIONS=5000 bin/benchmark`
364
-
365
- ## License
366
-
367
- MIT