fast_cov 0.3.2 → 0.4.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 +4 -4
- data/README.md +95 -33
- data/lib/fast_cov/test_map/aggregator.rb +124 -0
- data/lib/fast_cov/test_map/reader.rb +65 -0
- data/lib/fast_cov/test_map.rb +72 -0
- data/lib/fast_cov/version.rb +1 -1
- data/lib/fast_cov.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b276767c3b77601bb2cb06febd1476fcab8c5f94fc5b714842d0ddcce6a0fb9
|
|
4
|
+
data.tar.gz: 44bd1a0b268802dc8c7b6e5687bf52fcb969849191fdb0d5471cab87350e43cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 178d1061ae15c475719b2da16297df134be25798a6bc22ec81aece28a180d7db2e5914296fc54596d3ea858b57538be177dc96f203d2625004c2f369078b6d44
|
|
7
|
+
data.tar.gz: abdf03b462db505b675d29b332ddc933a6af415cef35a474608d9d8330993b52f294b84923a00dd7c9371bcd7311b3e1518e1ab9772088fbb529740ff74fbfbc
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ FastCov hooks directly into the Ruby VM's event system, avoiding the overhead of
|
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
|
-
- Ruby >= 3.
|
|
9
|
+
- Ruby >= 3.2.0 (MRI only)
|
|
10
10
|
- macOS or Linux
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
@@ -32,7 +32,7 @@ coverage = FastCov::CoverageMap.new
|
|
|
32
32
|
coverage.root = File.expand_path("app")
|
|
33
33
|
coverage.use(FastCov::FileTracker)
|
|
34
34
|
|
|
35
|
-
result = coverage.
|
|
35
|
+
result = coverage.build do
|
|
36
36
|
# ... run a test ...
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -53,6 +53,8 @@ coverage.ignored_paths = Rails.root.join("vendor")
|
|
|
53
53
|
|
|
54
54
|
coverage.use(FastCov::FileTracker)
|
|
55
55
|
coverage.use(FastCov::FactoryBotTracker)
|
|
56
|
+
coverage.use(FastCov::ConstGetTracker)
|
|
57
|
+
coverage.use(FastCov::FixtureKitTracker)
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
### Options
|
|
@@ -66,9 +68,13 @@ coverage.use(FastCov::FactoryBotTracker)
|
|
|
66
68
|
### Lifecycle
|
|
67
69
|
|
|
68
70
|
```ruby
|
|
69
|
-
coverage.start
|
|
70
|
-
coverage.stop
|
|
71
|
-
|
|
71
|
+
coverage.start # starts tracking, returns self
|
|
72
|
+
result = coverage.stop # stops tracking, returns a Set
|
|
73
|
+
|
|
74
|
+
# Block form: start, yield, stop
|
|
75
|
+
result = coverage.build do
|
|
76
|
+
# ...
|
|
77
|
+
end
|
|
72
78
|
```
|
|
73
79
|
|
|
74
80
|
Native line coverage is always enabled. Extra trackers registered with `use` are additive.
|
|
@@ -77,7 +83,11 @@ Native line coverage is always enabled. Extra trackers registered with `use` are
|
|
|
77
83
|
|
|
78
84
|
### FileTracker
|
|
79
85
|
|
|
80
|
-
Tracks files read from disk during coverage, including
|
|
86
|
+
Tracks files read from disk during coverage, including YAML, JSON, ERB templates, and any file accessed via `File.read`, read-mode `File.open`, `YAML.load_file`, `YAML.safe_load_file`, or `YAML.unsafe_load_file`.
|
|
87
|
+
|
|
88
|
+
The YAML methods are patched directly to handle Bootsnap's compile cache, which bypasses `File.open` for YAML files.
|
|
89
|
+
|
|
90
|
+
When a file is read indirectly (e.g., `YAML.load_file` calling through Psych), the tracker walks the caller stack to find the first in-root frame and creates a connected dependency.
|
|
81
91
|
|
|
82
92
|
```ruby
|
|
83
93
|
coverage.use(FastCov::FileTracker)
|
|
@@ -107,62 +117,112 @@ This catches patterns such as:
|
|
|
107
117
|
|
|
108
118
|
It does not catch direct constant references such as `Foo::Bar` in source code.
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
### FixtureKitTracker
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
Tracks [fixture_kit](https://github.com/Gusto/fixture_kit) fixture definition files when fixtures are used. Requires fixture_kit >= 0.14.0.
|
|
123
|
+
|
|
124
|
+
Fixture definitions run once during cache generation (`before(:context)`), then every test replays cached SQL without executing Ruby. This tracker uses fixture_kit's callback hooks to:
|
|
125
|
+
|
|
126
|
+
1. Track files touched during fixture generation and create connected dependencies
|
|
127
|
+
2. Record fixture definition files (including parent chain) when tests mount fixtures
|
|
113
128
|
|
|
114
129
|
```ruby
|
|
115
|
-
|
|
116
|
-
root: "/repo/app",
|
|
117
|
-
ignored_paths: ["/repo/app/vendor"],
|
|
118
|
-
threads: true
|
|
119
|
-
)
|
|
130
|
+
coverage.use(FastCov::FixtureKitTracker)
|
|
120
131
|
```
|
|
121
132
|
|
|
122
|
-
This API is mainly useful for internal use and low-level tests. `CoverageMap` is the intended public orchestration API.
|
|
123
|
-
|
|
124
133
|
## StaticMap
|
|
125
134
|
|
|
126
|
-
`FastCov::StaticMap` is a build-time API for static dependency mapping. It parses Ruby files with Prism, resolves literal constant references, and builds a
|
|
135
|
+
`FastCov::StaticMap` is a build-time API for static dependency mapping. It parses Ruby files with Prism, resolves literal constant references, and builds a dependency graph. Transitive closures are computed lazily on demand.
|
|
127
136
|
|
|
128
137
|
```ruby
|
|
129
138
|
static_map = FastCov::StaticMap.new(root: Rails.root)
|
|
130
139
|
static_map.build("spec/**/*_spec.rb")
|
|
131
140
|
|
|
132
141
|
# Direct dependencies for a single file
|
|
133
|
-
static_map.
|
|
134
|
-
# => ["
|
|
142
|
+
static_map.direct_dependencies("spec/models/user_spec.rb")
|
|
143
|
+
# => ["app/models/user.rb"]
|
|
135
144
|
|
|
136
|
-
# Transitive
|
|
137
|
-
static_map.
|
|
138
|
-
# => ["
|
|
139
|
-
|
|
140
|
-
# Raw direct graph
|
|
141
|
-
static_map.direct_graph
|
|
142
|
-
# => { "/app/spec/models/user_spec.rb" => ["/app/app/models/user.rb"], ... }
|
|
145
|
+
# Transitive dependencies (computed and cached on first call)
|
|
146
|
+
static_map.dependencies("spec/models/user_spec.rb")
|
|
147
|
+
# => ["app/models/user.rb", "app/models/account.rb"]
|
|
143
148
|
```
|
|
144
149
|
|
|
145
150
|
The instance caches constant resolution results, so reusing the same instance across multiple `build` calls is efficient.
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
### Options
|
|
148
153
|
|
|
149
154
|
| Option | Type | Default | Description |
|
|
150
155
|
|---|---|---|---|
|
|
151
156
|
| `root` | String or Pathname | required | Absolute project root. Only resolved files under this path are included. |
|
|
152
|
-
| `ignored_paths` | String or Array
|
|
153
|
-
|
|
|
157
|
+
| `ignored_paths` | String or Array | `[]` | Files or directories to exclude from the graph and recursive traversal. |
|
|
158
|
+
| `concurrency` | Integer | `Etc.nprocessors` | Number of threads for parallel file parsing. |
|
|
154
159
|
|
|
155
|
-
|
|
160
|
+
### How it works
|
|
156
161
|
|
|
157
|
-
- `build` traverses reachable files and stores a direct dependency graph
|
|
158
|
-
- `
|
|
159
|
-
- `
|
|
162
|
+
- `build(*patterns)` traverses reachable files and stores a direct dependency graph
|
|
163
|
+
- `direct_dependencies(file)` returns direct dependencies for a file
|
|
164
|
+
- `dependencies(file)` computes and caches the transitive closure lazily
|
|
160
165
|
- Constant resolution results are cached and reused across `build` calls
|
|
161
166
|
- Resolves each reference from most-specific lexical candidate to least-specific
|
|
162
167
|
- Uses `const_defined?` and `const_source_location` to resolve literal constant references to source files
|
|
163
168
|
|
|
164
169
|
This is intended for a booted application process. It requires constants to be eager-loaded. It will not see dynamic constant lookups that are not expressed as literal constants in the source.
|
|
165
170
|
|
|
171
|
+
## TestMap
|
|
172
|
+
|
|
173
|
+
`FastCov::TestMap` handles test mapping serialization and aggregation. It accumulates mappings from test runs, writes gzipped fragment files, and merges fragments from multiple CI nodes.
|
|
174
|
+
|
|
175
|
+
### Accumulating mappings
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
test_map = FastCov::TestMap.new
|
|
179
|
+
|
|
180
|
+
# Record which files each test depends on
|
|
181
|
+
test_map.add("spec/models/" => coverage_map.stop)
|
|
182
|
+
|
|
183
|
+
# Query: which tests cover this file?
|
|
184
|
+
test_map.dependencies("app/models/user.rb")
|
|
185
|
+
# => ["spec/models/"]
|
|
186
|
+
|
|
187
|
+
# Write gzipped fragment for later aggregation
|
|
188
|
+
test_map.dump("tmp/test_mapping.node_0.gz")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Aggregating fragments
|
|
192
|
+
|
|
193
|
+
Merge fragments from multiple CI nodes via k-way merge:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
aggregator = FastCov::TestMap.aggregate(Dir["tmp/test_mapping.*.gz"])
|
|
197
|
+
|
|
198
|
+
# Hook into progress events
|
|
199
|
+
aggregator.on(:sort) { |fragments, batches| puts "#{fragments} fragments -> #{batches} batches" }
|
|
200
|
+
aggregator.on(:sorted) { |elapsed| puts "Sorted in #{elapsed.round(2)}s" }
|
|
201
|
+
aggregator.on(:merge) { |processed, total| print "#{processed}/#{total}\r" }
|
|
202
|
+
aggregator.on(:merged) { |files, elapsed| puts "Merged #{files} files in #{elapsed.round(2)}s" }
|
|
203
|
+
|
|
204
|
+
# Iterate in batches — yields Hash of { file => [deps] }
|
|
205
|
+
aggregator.each(10_000) do |batch|
|
|
206
|
+
database.bulk_write(batch)
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Options
|
|
211
|
+
|
|
212
|
+
| Option | Type | Default | Description |
|
|
213
|
+
|---|---|---|---|
|
|
214
|
+
| `readers:` | Integer | `min(100, ulimit/2)` | Max concurrent readers for k-way merge. Auto-detected from OS file descriptor limit. |
|
|
215
|
+
|
|
216
|
+
### Fragment format
|
|
217
|
+
|
|
218
|
+
Tab-delimited, gzipped. One line per source file, first column is the file, remaining columns are dependencies:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
source_file\tdep1\tdep2\tdep3
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Aggregation owns sorting — fragments are unsorted, intermediates are sorted during the merge process using pure Ruby (no shell commands).
|
|
225
|
+
|
|
166
226
|
## Writing custom trackers
|
|
167
227
|
|
|
168
228
|
There are two approaches: a minimal custom tracker, or inheriting from `AbstractTracker`.
|
|
@@ -200,6 +260,7 @@ end
|
|
|
200
260
|
- thread-aware recording
|
|
201
261
|
- lifecycle management
|
|
202
262
|
- class-level `record` dispatch for patched hooks
|
|
263
|
+
- caller stack traversal via `Utils.resolve_caller` for indirect calls
|
|
203
264
|
|
|
204
265
|
```ruby
|
|
205
266
|
class MyTracker < FastCov::AbstractTracker
|
|
@@ -209,7 +270,8 @@ class MyTracker < FastCov::AbstractTracker
|
|
|
209
270
|
|
|
210
271
|
module MyPatch
|
|
211
272
|
def some_method(...)
|
|
212
|
-
|
|
273
|
+
# record(path) auto-resolves the caller via stack traversal
|
|
274
|
+
MyTracker.record(some_file_path)
|
|
213
275
|
super
|
|
214
276
|
end
|
|
215
277
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
6
|
+
module FastCov
|
|
7
|
+
class TestMap
|
|
8
|
+
# Handles k-way merge of sorted fragment files.
|
|
9
|
+
# Created by TestMap.aggregate, not instantiated directly.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# aggregator = FastCov::TestMap.aggregate(Dir["tmp/test_mapping.*.gz"])
|
|
13
|
+
# aggregator.on(:sorted) { |elapsed| puts "Sorted in #{elapsed.round(2)}s" }
|
|
14
|
+
# aggregator.on(:merged) { |files, elapsed| puts "Merged #{files} files in #{elapsed.round(2)}s" }
|
|
15
|
+
# aggregator.each(10_000) { |batch| database.bulk_write(batch) }
|
|
16
|
+
class Aggregator
|
|
17
|
+
def initialize(fragment_paths, max_readers)
|
|
18
|
+
@fragment_paths = fragment_paths
|
|
19
|
+
@max_readers = max_readers
|
|
20
|
+
@hooks = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Register a callback for an aggregation event.
|
|
24
|
+
#
|
|
25
|
+
# Events:
|
|
26
|
+
# :sort — before sorting. Yields (fragment_count, batch_count)
|
|
27
|
+
# :sorted — after sorting. Yields (elapsed)
|
|
28
|
+
# :merge — during merge. Yields (processed_lines, total_lines)
|
|
29
|
+
# :merged — after merging. Yields (file_count, elapsed)
|
|
30
|
+
def on(event, &block)
|
|
31
|
+
@hooks[event] = block
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Iterate over merged results.
|
|
36
|
+
# Yields a Hash of { file => dependencies } per batch.
|
|
37
|
+
# Default batch_size is 1.
|
|
38
|
+
def each(batch_size = 1, &block)
|
|
39
|
+
raise ArgumentError, "each requires a block" unless block
|
|
40
|
+
return if @fragment_paths.empty?
|
|
41
|
+
|
|
42
|
+
Dir.mktmpdir("fastcov") do |tmpdir|
|
|
43
|
+
intermediates, total_lines = create_intermediates(tmpdir)
|
|
44
|
+
readers = intermediates.map { |f| Reader.new(f) }
|
|
45
|
+
kway_merge(readers, batch_size, total_lines, &block)
|
|
46
|
+
ensure
|
|
47
|
+
readers&.each(&:close)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def emit(event, *args)
|
|
54
|
+
@hooks[event]&.call(*args)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def measure
|
|
58
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
59
|
+
result = yield
|
|
60
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
61
|
+
[result, elapsed]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def create_intermediates(intermediates_dir)
|
|
65
|
+
batch_size = (@fragment_paths.size.to_f / @max_readers).ceil
|
|
66
|
+
batches = @fragment_paths.each_slice(batch_size).to_a
|
|
67
|
+
|
|
68
|
+
emit(:sort, @fragment_paths.size, batches.size)
|
|
69
|
+
|
|
70
|
+
total_lines = 0
|
|
71
|
+
intermediates, elapsed = measure do
|
|
72
|
+
batches.each_with_index.map do |batch, i|
|
|
73
|
+
intermediate = File.join(intermediates_dir, "intermediate_#{i}.txt")
|
|
74
|
+
lines = batch.flat_map { |f| Zlib::GzipReader.open(f) { |gz| gz.readlines } }
|
|
75
|
+
total_lines += lines.size
|
|
76
|
+
lines.sort!
|
|
77
|
+
File.write(intermediate, lines.join)
|
|
78
|
+
intermediate
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
emit(:sorted, elapsed)
|
|
83
|
+
[intermediates, total_lines]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def kway_merge(readers, batch_size, total_lines, &block)
|
|
87
|
+
unique_files = 0
|
|
88
|
+
processed_lines = 0
|
|
89
|
+
batch = {}
|
|
90
|
+
|
|
91
|
+
_, elapsed = measure do
|
|
92
|
+
loop do
|
|
93
|
+
active = readers.reject(&:exhausted?)
|
|
94
|
+
break if active.empty?
|
|
95
|
+
|
|
96
|
+
min_path = active.map(&:file_path).min
|
|
97
|
+
|
|
98
|
+
merged = Set.new
|
|
99
|
+
active.each do |reader|
|
|
100
|
+
if reader.file_path == min_path
|
|
101
|
+
processed_lines += 1
|
|
102
|
+
merged.merge(reader.dependencies)
|
|
103
|
+
reader.advance
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
emit(:merge, processed_lines, total_lines)
|
|
108
|
+
unique_files += 1
|
|
109
|
+
|
|
110
|
+
batch[min_path] = merged.to_a.sort
|
|
111
|
+
if batch.size >= batch_size
|
|
112
|
+
block.call(batch)
|
|
113
|
+
batch = {}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
block.call(batch) unless batch.empty?
|
|
119
|
+
|
|
120
|
+
emit(:merged, unique_files, elapsed)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module FastCov
|
|
6
|
+
class TestMap
|
|
7
|
+
# Reads a single sorted fragment file (gzipped or plain text).
|
|
8
|
+
# Merges consecutive entries with the same file path, which occur when
|
|
9
|
+
# multiple fragments are concatenated and sorted into an intermediate.
|
|
10
|
+
class Reader
|
|
11
|
+
attr_reader :file_path, :dependencies
|
|
12
|
+
|
|
13
|
+
def initialize(path)
|
|
14
|
+
@io = path.to_s.end_with?(".gz") ? Zlib::GzipReader.open(path) : File.open(path)
|
|
15
|
+
@exhausted = false
|
|
16
|
+
@next_file_path = nil
|
|
17
|
+
@next_dependencies = nil
|
|
18
|
+
read_line
|
|
19
|
+
advance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def exhausted?
|
|
23
|
+
@exhausted
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def advance
|
|
27
|
+
if @next_file_path.nil?
|
|
28
|
+
@exhausted = true
|
|
29
|
+
@file_path = nil
|
|
30
|
+
@dependencies = []
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@file_path = @next_file_path
|
|
35
|
+
@dependencies = @next_dependencies
|
|
36
|
+
read_line
|
|
37
|
+
|
|
38
|
+
# Merge consecutive lines with the same file path
|
|
39
|
+
while @next_file_path == @file_path
|
|
40
|
+
@dependencies.concat(@next_dependencies)
|
|
41
|
+
read_line
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def close
|
|
46
|
+
@io.close
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def read_line
|
|
52
|
+
line = @io.gets
|
|
53
|
+
if line.nil?
|
|
54
|
+
@next_file_path = nil
|
|
55
|
+
@next_dependencies = nil
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
parts = line.chomp.split("\t")
|
|
60
|
+
@next_file_path = parts[0]
|
|
61
|
+
@next_dependencies = parts[1..] || []
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
6
|
+
module FastCov
|
|
7
|
+
# In-memory test mapping that records which files each test depends on.
|
|
8
|
+
# Can be dumped to a gzipped TSV fragment file for later aggregation.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# # Accumulate mappings (e.g., in an RSpec formatter)
|
|
12
|
+
# test_map = FastCov::TestMap.new
|
|
13
|
+
# test_map.add("spec/models/user_spec.rb" => coverage_result)
|
|
14
|
+
# test_map.dump("tmp/test_mapping.node_0.gz")
|
|
15
|
+
#
|
|
16
|
+
# # Query mappings
|
|
17
|
+
# test_map.dependencies("app/models/user.rb")
|
|
18
|
+
# # => ["spec/models/user_spec.rb"]
|
|
19
|
+
#
|
|
20
|
+
# # Aggregate fragments from multiple nodes
|
|
21
|
+
# aggregator = FastCov::TestMap.aggregate(Dir["tmp/test_mapping.*.gz"])
|
|
22
|
+
# aggregator.on(:sorted) { |elapsed| puts "Sorted in #{elapsed.round(2)}s" }
|
|
23
|
+
# aggregator.on(:merged) { |files, elapsed| puts "Merged #{files} files in #{elapsed.round(2)}s" }
|
|
24
|
+
# aggregator.each(10_000) { |batch| database.bulk_write(batch) }
|
|
25
|
+
class TestMap
|
|
26
|
+
autoload :Aggregator, File.expand_path("test_map/aggregator", __dir__)
|
|
27
|
+
autoload :Reader, File.expand_path("test_map/reader", __dir__)
|
|
28
|
+
|
|
29
|
+
DEFAULT_MAX_READERS = [100, Process.getrlimit(Process::RLIMIT_NOFILE).first / 2].min
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@mapping = {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Record test -> dependency mappings.
|
|
36
|
+
# Accepts a Hash of { test_path => dependencies }.
|
|
37
|
+
def add(mappings)
|
|
38
|
+
mappings.each do |test_path, deps|
|
|
39
|
+
deps.each do |dep|
|
|
40
|
+
next if dep == test_path
|
|
41
|
+
|
|
42
|
+
(@mapping[dep] ||= Set.new) << test_path
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the test paths that depend on the given file.
|
|
48
|
+
def dependencies(file)
|
|
49
|
+
@mapping[file]&.to_a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Write the accumulated mappings as a gzipped TSV fragment.
|
|
53
|
+
def dump(path)
|
|
54
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
55
|
+
|
|
56
|
+
lines = @mapping.map { |file, deps| "#{file}\t#{deps.to_a.join("\t")}\n" }
|
|
57
|
+
Zlib::GzipWriter.open(path) { |gz| gz.write(lines.join) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Number of unique source files mapped.
|
|
61
|
+
def size
|
|
62
|
+
@mapping.size
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Create an Aggregator for merging fragment files.
|
|
66
|
+
# Accepts file paths or glob patterns.
|
|
67
|
+
def self.aggregate(*patterns, readers: DEFAULT_MAX_READERS)
|
|
68
|
+
fragment_paths = patterns.flatten.flat_map { |p| p.include?("*") ? Dir.glob(p).sort : p }
|
|
69
|
+
Aggregator.new(fragment_paths, readers)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/fast_cov/version.rb
CHANGED
data/lib/fast_cov.rb
CHANGED
|
@@ -13,4 +13,5 @@ module FastCov
|
|
|
13
13
|
autoload :ConstGetTracker, File.expand_path("fast_cov/trackers/const_get_tracker", __dir__)
|
|
14
14
|
autoload :FixtureKitTracker, File.expand_path("fast_cov/trackers/fixture_kit_tracker", __dir__)
|
|
15
15
|
autoload :StaticMap, File.expand_path("fast_cov/static_map", __dir__)
|
|
16
|
+
autoload :TestMap, File.expand_path("fast_cov/test_map", __dir__)
|
|
16
17
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast_cov
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ngan Pham
|
|
@@ -115,6 +115,9 @@ files:
|
|
|
115
115
|
- lib/fast_cov/dev.rb
|
|
116
116
|
- lib/fast_cov/static_map.rb
|
|
117
117
|
- lib/fast_cov/static_map/reference_extractor.rb
|
|
118
|
+
- lib/fast_cov/test_map.rb
|
|
119
|
+
- lib/fast_cov/test_map/aggregator.rb
|
|
120
|
+
- lib/fast_cov/test_map/reader.rb
|
|
118
121
|
- lib/fast_cov/trackers/abstract_tracker.rb
|
|
119
122
|
- lib/fast_cov/trackers/const_get_tracker.rb
|
|
120
123
|
- lib/fast_cov/trackers/factory_bot_tracker.rb
|