fast_cov 0.3.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a8e169598d457f54b52e872c65d55a9e87164bcac66654d7b21d9497aeba43a
4
- data.tar.gz: 678150e27c7fb664bc5786764b063c1abb75218048f226b39495bc97c012ce75
3
+ metadata.gz: 8d5eb20089cfe981168592cd5672e1919746a5186c61f151aff87eea7319eb1c
4
+ data.tar.gz: af45acb129fd680beaa1a906248ad1aedf03e10a5d40d02fbeaa0971e9112b44
5
5
  SHA512:
6
- metadata.gz: 68e671eab2e3583313b60e408f179006d77aa0d571d1b258b971916b38642fcedae05b8b728ff0588000bd69ada0183a3502b8f6b4e90a37fda3847be3dd6fcd
7
- data.tar.gz: d4531b99a2ca2cfb1a566f63bed51444a91bedfd0356a33d50101f18aafa0020ed40d652199de538cb351c6ac2e5e6a7185c1a3aaef316bc444779767a19ad23
6
+ metadata.gz: 97cdc30f83fb1970d97d6cddc04758463043a85ee81ccd5a22011e0b017a04d8b9b82be90931b42bdf5565ef163ac84cc1544ec43ecae2cc1727041770adfc52
7
+ data.tar.gz: baa28e4ae365439250404b285eb0577a167de1cb907a3c37b33f09d3a99bf53bf4e6ca0ac6fd2ff84360a048f2795de7ca709ffd11c1236b0846ae66a4f4a75a
@@ -140,80 +140,6 @@ static void on_line_event(rb_event_flag_t event, VALUE self_data, VALUE self,
140
140
  record_impacted_file(data, filename);
141
141
  }
142
142
 
143
- // ---- Utils module methods (FastCov::Utils) ------------------------------
144
-
145
- // Utils.path_within?(path, directory) -> true/false
146
- // Check if path is within directory, correctly handling:
147
- // - Trailing slashes on directory
148
- // - Sibling directories with longer names (e.g., /a/b/c vs /a/b/cd)
149
- static VALUE utils_path_within(VALUE self, VALUE path, VALUE directory) {
150
- Check_Type(path, T_STRING);
151
- Check_Type(directory, T_STRING);
152
-
153
- // Freeze strings to prevent GC compaction from moving them
154
- rb_str_freeze(path);
155
- rb_str_freeze(directory);
156
-
157
- bool result = fast_cov_is_within_root(
158
- RSTRING_PTR(path), RSTRING_LEN(path),
159
- RSTRING_PTR(directory), RSTRING_LEN(directory));
160
-
161
- return result ? Qtrue : Qfalse;
162
- }
163
-
164
- // Utils.relativize_paths(set, root) -> set
165
- // Mutates set in place: converts absolute paths to relative paths from root.
166
- // Paths not within root are left unchanged.
167
- static VALUE utils_relativize_paths(VALUE self, VALUE set, VALUE root) {
168
- Check_Type(root, T_STRING);
169
-
170
- // Freeze root to prevent GC from moving it during compaction
171
- rb_str_freeze(root);
172
-
173
- const char *root_ptr = RSTRING_PTR(root);
174
- long root_len = RSTRING_LEN(root);
175
-
176
- // Normalize: strip trailing slash for offset calculation
177
- long effective_root_len = root_len;
178
- if (effective_root_len > 0 && root_ptr[effective_root_len - 1] == '/') {
179
- effective_root_len--;
180
- }
181
-
182
- // Collect paths to transform (can't modify set while iterating)
183
- VALUE paths = rb_funcall(set, rb_intern("to_a"), 0);
184
- long num_paths = RARRAY_LEN(paths);
185
-
186
- for (long i = 0; i < num_paths; i++) {
187
- VALUE abs_path = rb_ary_entry(paths, i);
188
- if (!RB_TYPE_P(abs_path, T_STRING)) continue;
189
-
190
- // Freeze to prevent GC moving it
191
- rb_str_freeze(abs_path);
192
-
193
- const char *path_ptr = RSTRING_PTR(abs_path);
194
- long path_len = RSTRING_LEN(abs_path);
195
-
196
- // Use proper within_root check
197
- if (!fast_cov_is_within_root(path_ptr, path_len, root_ptr, root_len)) {
198
- continue;
199
- }
200
-
201
- // Calculate offset (skip root + separator)
202
- long offset = effective_root_len;
203
- if (offset < path_len && path_ptr[offset] == '/') offset++;
204
-
205
- // Create relative path
206
- VALUE rel_path = rb_str_substr(abs_path, offset, path_len - offset);
207
-
208
- // Delete old path, add new path
209
- rb_funcall(set, rb_intern("delete"), 1, abs_path);
210
- rb_funcall(set, rb_intern("add"), 1, rel_path);
211
- }
212
-
213
- RB_GC_GUARD(paths);
214
- return set;
215
- }
216
-
217
143
  // ---- Ruby instance methods ----------------------------------------------
218
144
 
219
145
  static VALUE fast_cov_initialize(int argc, VALUE *argv, VALUE self) {
@@ -366,8 +292,4 @@ void Init_fast_cov(void) {
366
292
  rb_define_method(cCoverage, "start", fast_cov_start, 0);
367
293
  rb_define_method(cCoverage, "stop", fast_cov_stop, 0);
368
294
 
369
- // FastCov::Utils module (C-defined methods)
370
- VALUE mUtils = rb_define_module_under(mFastCov, "Utils");
371
- rb_define_module_function(mUtils, "path_within?", utils_path_within, 2);
372
- rb_define_module_function(mUtils, "relativize_paths", utils_relativize_paths, 2);
373
295
  }
@@ -15,12 +15,18 @@ module FastCov
15
15
  # - threads: true -> record from ALL threads (global tracking)
16
16
  # - threads: false -> only record from the thread that called start
17
17
  class AbstractTracker
18
+ attr_reader :coverage_map
19
+
18
20
  def initialize(coverage_map, **_options)
19
21
  @coverage_map = coverage_map
20
22
  @files = nil
21
23
  @started_thread = nil
22
24
  end
23
25
 
26
+ def root
27
+ coverage_map.root
28
+ end
29
+
24
30
  # Public API - called by FastCov framework
25
31
 
26
32
  def start
@@ -70,12 +76,12 @@ module FastCov
70
76
  class << self
71
77
  attr_accessor :active
72
78
 
73
- def record(to: nil)
79
+ def record(path, to: nil)
74
80
  return unless active
75
- return unless block_given?
81
+ return unless path
76
82
 
77
- path = yield
78
- active.record(path, to: to) if path
83
+ to ||= Utils.resolve_caller(caller_locations(1, 20), active.root)
84
+ active.record(path, to: to)
79
85
  end
80
86
 
81
87
  def reset
@@ -23,9 +23,8 @@ module FastCov
23
23
 
24
24
  module ConstGetPatch
25
25
  def const_get(name, inherit = true)
26
- source = caller_locations(1, 1).first&.absolute_path
27
26
  result = super
28
- FastCov::ConstGetTracker.record(to: source) { const_source_location(name, inherit)&.first }
27
+ FastCov::ConstGetTracker.record(const_source_location(name, inherit)&.first)
29
28
  result
30
29
  end
31
30
  end
@@ -12,9 +12,7 @@ module FastCov
12
12
  # Register via: coverage_map.use(FastCov::FactoryBotTracker)
13
13
  class FactoryBotTracker < AbstractTracker
14
14
  def install
15
- unless defined?(::FactoryBot)
16
- raise LoadError, "FactoryBotTracker requires the factory_bot gem to be installed"
17
- end
15
+ gem "factory_bot"
18
16
 
19
17
  return if ::FactoryBot.factories.singleton_class.ancestors.include?(RegistryPatch)
20
18
 
@@ -42,7 +40,7 @@ module FastCov
42
40
  next unless block.is_a?(Proc)
43
41
 
44
42
  location = block.source_location
45
- record { location&.first }
43
+ record(location&.first)
46
44
  end
47
45
  end
48
46
  end
@@ -4,21 +4,27 @@ require_relative "abstract_tracker"
4
4
 
5
5
  module FastCov
6
6
  # Tracks files read from disk during coverage (JSON, YAML, .rb templates, etc.)
7
- # via File.read and File.open.
7
+ # via File.read, File.open, and YAML load methods.
8
+ #
9
+ # YAML methods are patched separately because Bootsnap's compile cache
10
+ # overrides YAML.load_file/safe_load_file to bypass File.open entirely.
8
11
  #
9
12
  # Register via: coverage_map.use(FastCov::FileTracker)
10
13
  class FileTracker < AbstractTracker
11
14
  def install
12
- return if File.singleton_class.ancestors.include?(FilePatch)
15
+ unless File.singleton_class.ancestors.include?(FilePatch)
16
+ File.singleton_class.prepend(FilePatch)
17
+ end
13
18
 
14
- File.singleton_class.prepend(FilePatch)
19
+ if defined?(::YAML) && !::YAML.singleton_class.ancestors.include?(YamlPatch)
20
+ ::YAML.singleton_class.prepend(YamlPatch)
21
+ end
15
22
  end
16
23
 
17
24
  module FilePatch
18
25
  def read(name, *args, **kwargs, &block)
19
- source = caller_locations(1, 1).first&.absolute_path
20
26
  super.tap do
21
- FastCov::FileTracker.record(to: source) { File.expand_path(name) }
27
+ FastCov::FileTracker.record(File.expand_path(name))
22
28
  end
23
29
  end
24
30
 
@@ -26,9 +32,28 @@ module FastCov
26
32
  mode = args[0]
27
33
  is_read = mode.nil? || (mode.is_a?(String) && mode.start_with?("r")) ||
28
34
  (mode.is_a?(Integer) && (mode & (File::WRONLY | File::RDWR)).zero?)
29
- source = caller_locations(1, 1).first&.absolute_path
30
35
  super.tap do
31
- FastCov::FileTracker.record(to: source) { File.expand_path(name) } if is_read
36
+ FastCov::FileTracker.record(File.expand_path(name)) if is_read
37
+ end
38
+ end
39
+ end
40
+
41
+ module YamlPatch
42
+ def load_file(path, *args, **kwargs)
43
+ super.tap do
44
+ FastCov::FileTracker.record(File.expand_path(path))
45
+ end
46
+ end
47
+
48
+ def safe_load_file(path, *args, **kwargs)
49
+ super.tap do
50
+ FastCov::FileTracker.record(File.expand_path(path))
51
+ end
52
+ end
53
+
54
+ def unsafe_load_file(path, *args, **kwargs)
55
+ super.tap do
56
+ FastCov::FileTracker.record(File.expand_path(path))
32
57
  end
33
58
  end
34
59
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract_tracker"
4
+
5
+ module FastCov
6
+ # Tracks FixtureKit fixture definition files when fixtures are used.
7
+ #
8
+ # Fixture definitions run once during cache generation (before(:context)),
9
+ # then every test replays cached SQL without executing Ruby. This tracker
10
+ # uses FixtureKit's callback hooks to:
11
+ #
12
+ # 1. Connect the fixture file to all source files touched during generation
13
+ # 2. Record the fixture definition file when a test mounts a fixture
14
+ #
15
+ # Requires fixture_kit >= 0.14.0 (Event-based callbacks).
16
+ #
17
+ # Register via: coverage_map.use(FastCov::FixtureKitTracker)
18
+ class FixtureKitTracker < AbstractTracker
19
+ def install
20
+ gem "fixture_kit", ">= 0.14.0"
21
+
22
+ tracker = self
23
+
24
+ FixtureKit.configure do |config|
25
+ # When a fixture is about to be generated, start the coverage map
26
+ # to track what files the fixture definition touches.
27
+ # This runs in before(:context), before the formatter starts coverage.
28
+ config.on_cache_save do |_fixture|
29
+ tracker.coverage_map.start
30
+ end
31
+
32
+ # After generation, stop coverage and connect the fixture file
33
+ # to everything it touched.
34
+ config.on_cache_saved do |fixture, _duration|
35
+ files = tracker.coverage_map.stop
36
+ files.each do |file|
37
+ tracker.coverage_map.connect(from: fixture.path, to: file)
38
+ end
39
+ end
40
+
41
+ # When a test mounts a fixture, record the fixture definition file
42
+ # and any parent fixture files in the chain.
43
+ config.on_cache_mount do |event|
44
+ tracker.class.record(event.path)
45
+ parent = event.fixture.parent
46
+ while parent
47
+ tracker.class.record(parent.definition.path)
48
+ parent = parent.parent
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastCov
4
+ module Utils
5
+ # Check if path is within directory, correctly handling:
6
+ # - Trailing slashes on directory
7
+ # - Sibling directories with longer names (e.g., /a/b/c vs /a/b/cd)
8
+ def self.path_within?(path, directory)
9
+ dir = directory.end_with?("/") ? directory.chop : directory
10
+ return true if path == dir
11
+
12
+ path.start_with?("#{dir}/")
13
+ end
14
+
15
+ # Mutates set in place: converts absolute paths to relative paths from root.
16
+ # Paths not within root are left unchanged.
17
+ def self.relativize_paths(set, root)
18
+ prefix = root.end_with?("/") ? root : "#{root}/"
19
+
20
+ set.to_a.each do |abs_path|
21
+ next unless abs_path.is_a?(String)
22
+ next unless abs_path.start_with?(prefix) || abs_path == root.chomp("/")
23
+
24
+ set.delete(abs_path)
25
+ set.add(abs_path.delete_prefix(prefix))
26
+ end
27
+
28
+ set
29
+ end
30
+
31
+ # Walk caller locations to find the first frame whose file is within root.
32
+ # Handles indirect calls (e.g., YAML.load_file -> File.open) where the
33
+ # immediate caller is a stdlib/gem file outside the project.
34
+ def self.resolve_caller(locations, root)
35
+ locations.each do |loc|
36
+ path = loc.absolute_path
37
+ next unless path
38
+
39
+ return path if path_within?(path, root)
40
+ end
41
+ nil
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCov
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
data/lib/fast_cov.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "fast_cov/fast_cov.#{RUBY_VERSION}"
4
4
 
5
5
  module FastCov
6
+ autoload :Utils, File.expand_path("fast_cov/utils", __dir__)
6
7
  autoload :VERSION, File.expand_path("fast_cov/version", __dir__)
7
8
  autoload :ConnectedDependencies, File.expand_path("fast_cov/connected_dependencies", __dir__)
8
9
  autoload :CoverageMap, File.expand_path("fast_cov/coverage_map", __dir__)
@@ -10,5 +11,6 @@ module FastCov
10
11
  autoload :FileTracker, File.expand_path("fast_cov/trackers/file_tracker", __dir__)
11
12
  autoload :FactoryBotTracker, File.expand_path("fast_cov/trackers/factory_bot_tracker", __dir__)
12
13
  autoload :ConstGetTracker, File.expand_path("fast_cov/trackers/const_get_tracker", __dir__)
14
+ autoload :FixtureKitTracker, File.expand_path("fast_cov/trackers/fixture_kit_tracker", __dir__)
13
15
  autoload :StaticMap, File.expand_path("fast_cov/static_map", __dir__)
14
16
  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.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ngan Pham
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '6.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: fixture_kit
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 0.14.0
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 0.14.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: sqlite3
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
68
96
  description: A high-performance C extension that tracks which Ruby source files are
69
97
  executed during test runs, enabling test impact analysis.
70
98
  executables: []
@@ -91,6 +119,8 @@ files:
91
119
  - lib/fast_cov/trackers/const_get_tracker.rb
92
120
  - lib/fast_cov/trackers/factory_bot_tracker.rb
93
121
  - lib/fast_cov/trackers/file_tracker.rb
122
+ - lib/fast_cov/trackers/fixture_kit_tracker.rb
123
+ - lib/fast_cov/utils.rb
94
124
  - lib/fast_cov/version.rb
95
125
  homepage: https://github.com/Gusto/fast_cov
96
126
  licenses: