fast_cov 0.1.4 → 0.1.5

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: 19e4e4403a0d37409a7c3c71ab93499a288f7868d68756d7e906dfd8c8033ec3
4
- data.tar.gz: 8b1984d267cade376fea5c949f98d031d586b11ebf2e17096d749957a1d99af7
3
+ metadata.gz: 3ce0e302b02e7989034b39bd84cc69e7563378edb3ec31d10491899a4da74bac
4
+ data.tar.gz: 7273c905846579200383b0409d91ae8ea8066102e1c91d50af5a4c601e03f06e
5
5
  SHA512:
6
- metadata.gz: 7f1329d14f6e96f054db4e36b24addd60db315f63029b3be39d295667079b41fca240fb36fbdf6bc3cc7f5e80851e4db23557370b34728fb1b49e11cfb17d1a3
7
- data.tar.gz: 41af227e9377d3ac3277f658405561ca7d1ec4ae86c9a1306029fdc34dc7e315a63504f9503305d110d3efaac4503b6e90bcbe0d3f8d864e929322fff38b9a2f
6
+ metadata.gz: 149d759f1b8005b69741319655c2366b8ff3f0422b8c978418942e50f67a0b81b6077e7810c0a70aad56bf80fe130f2fc7b4003f55dc28ac0780d3ce2b847dcb
7
+ data.tar.gz: 6b8a1e08762f8fdcc4d2488f9c49348c38f5701499fd96550fdb4362cf64089ca76e55ecbfb6d50523cb190e15ed588b4162e873b3419b238d3a1680a6f97016
data/README.md CHANGED
@@ -51,8 +51,8 @@ Call `FastCov.configure` before using `start`/`stop`. The block yields a `Config
51
51
 
52
52
  ```ruby
53
53
  FastCov.configure do |config|
54
- config.root = Rails.root.to_s
55
- config.ignored_path = Rails.root.join("vendor").to_s
54
+ config.root = Rails.root
55
+ config.ignored_path = Rails.root.join("vendor")
56
56
  config.threads = true
57
57
 
58
58
  config.use FastCov::CoverageTracker
@@ -94,7 +94,7 @@ FastCov.reset # Clear configuration and trackers.
94
94
  ```ruby
95
95
  # spec/support/fast_cov.rb
96
96
  FastCov.configure do |config|
97
- config.root = Rails.root.to_s
97
+ config.root = Rails.root
98
98
  config.use FastCov::CoverageTracker
99
99
  config.use FastCov::FileTracker
100
100
  end
@@ -133,7 +133,7 @@ config.use FastCov::CoverageTracker
133
133
 
134
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
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.
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
137
 
138
138
  #### Disabling expensive features
139
139
 
@@ -189,6 +189,34 @@ config.use FastCov::FactoryBotTracker
189
189
 
190
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
191
 
192
+ ### ConstGetTracker
193
+
194
+ Tracks constants looked up dynamically via `Module#const_get`. This catches dynamic constant lookups that static analysis (Prism) would miss.
195
+
196
+ ```ruby
197
+ config.use FastCov::ConstGetTracker
198
+ ```
199
+
200
+ #### What it catches
201
+
202
+ - `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
205
+
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.
207
+
208
+ #### Options
209
+
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. |
215
+
216
+ #### How it works
217
+
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.
219
+
192
220
  ## Writing custom trackers
193
221
 
194
222
  There are two approaches to writing custom trackers: from scratch (minimal interface) or inheriting from `AbstractTracker` (batteries included).
@@ -295,7 +323,7 @@ Results from all trackers are merged, with later trackers overwriting earlier on
295
323
 
296
324
  ## Cache
297
325
 
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`.
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`.
299
327
 
300
328
  ```ruby
301
329
  FastCov::Cache.data # the raw cache hash
@@ -1,27 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  module FastCov
4
6
  class Configuration
5
- attr_accessor :root, :ignored_path, :threads
7
+ class ConfigurationError < StandardError; end
8
+
9
+ attr_accessor :threads
10
+ attr_reader :root, :ignored_path
6
11
 
7
12
  def initialize
8
13
  @root = Dir.pwd
9
14
  @ignored_path = nil
10
15
  @threads = true
11
- @trackers = []
16
+ @tracker_definitions = []
17
+ end
18
+
19
+ def root=(value)
20
+ return @root = nil if value.nil?
21
+
22
+ path = value.to_s
23
+ unless absolute_path?(path)
24
+ raise ConfigurationError, "root must be an absolute path, got: #{path.inspect}"
25
+ end
26
+ @root = path
27
+ end
28
+
29
+ def ignored_path=(value)
30
+ return @ignored_path = nil if value.nil?
31
+
32
+ path = value.to_s
33
+
34
+ # Expand relative paths against root
35
+ unless absolute_path?(path)
36
+ path = File.join(@root, path)
37
+ end
38
+
39
+ # Validate ignored_path is inside root
40
+ unless path.start_with?(@root)
41
+ raise ConfigurationError,
42
+ "ignored_path must be inside root (#{@root.inspect}), got: #{path.inspect}"
43
+ end
44
+
45
+ @ignored_path = path
12
46
  end
13
47
 
14
48
  def use(tracker_class, **options)
15
- @trackers << {klass: tracker_class, options: options}
49
+ @tracker_definitions << {klass: tracker_class, options: options}
16
50
  end
17
51
 
18
- def trackers
19
- @trackers
52
+ def install_trackers
53
+ @tracker_definitions.map do |entry|
54
+ tracker = entry[:klass].new(self, **entry[:options])
55
+ tracker.install if tracker.respond_to?(:install)
56
+ tracker
57
+ end
20
58
  end
21
59
 
22
60
  def reset
23
61
  initialize
24
62
  self
25
63
  end
64
+
65
+ private
66
+
67
+ def absolute_path?(path)
68
+ Pathname.new(path).absolute?
69
+ end
26
70
  end
27
71
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastCov
4
+ module Singleton
5
+ def configured?
6
+ !@trackers.nil? && !@trackers.empty?
7
+ end
8
+
9
+ def configure
10
+ @configuration = Configuration.new
11
+ yield(@configuration)
12
+ @trackers = @configuration.install_trackers
13
+ self
14
+ end
15
+
16
+ def start(&block)
17
+ raise "FastCov.configure must be called before start" unless configured?
18
+ @trackers.each(&:start)
19
+ if block
20
+ result = nil
21
+ begin
22
+ yield
23
+ ensure
24
+ result = stop
25
+ end
26
+ result
27
+ else
28
+ self
29
+ end
30
+ end
31
+
32
+ def stop
33
+ raise "FastCov.configure must be called before stop" unless configured?
34
+ result = Set.new
35
+ @trackers.each { |t| result.merge(t.stop) }
36
+ Utils.relativize_paths(result, @configuration.root)
37
+ end
38
+
39
+ def reset
40
+ @trackers = nil
41
+ @configuration = nil
42
+ end
43
+ end
44
+ end
@@ -43,8 +43,8 @@ module FastCov
43
43
 
44
44
  def record(abs_path)
45
45
  return if !@threads && Thread.current != @started_thread
46
- return unless abs_path.start_with?(@root)
47
- return if @ignored_path && abs_path.start_with?(@ignored_path)
46
+ return unless Utils.path_within?(abs_path, @root)
47
+ return if @ignored_path && Utils.path_within?(abs_path, @ignored_path)
48
48
  @files.add(abs_path) if on_record(abs_path)
49
49
  end
50
50
 
@@ -61,8 +61,19 @@ module FastCov
61
61
  class << self
62
62
  attr_accessor :active
63
63
 
64
- def record(abs_path)
65
- @active&.record(abs_path)
64
+ # Record a file path. Accepts a path directly or a block that returns the path.
65
+ # If block given, it's only executed when tracker is active (avoids expensive work).
66
+ # Nil values are ignored.
67
+ #
68
+ # record("/path/to/file.rb") # direct path
69
+ # record { expensive_lookup } # lazy evaluation
70
+ # record("/path") { fallback } # path takes precedence
71
+ #
72
+ def record(abs_path = nil)
73
+ return unless active
74
+
75
+ path = abs_path || (yield if block_given?)
76
+ active.record(path) if path
66
77
  end
67
78
 
68
79
  def reset
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract_tracker"
4
+
5
+ module FastCov
6
+ # Tracks constants looked up dynamically via Module#const_get.
7
+ #
8
+ # This catches dynamic constant lookups that static analysis misses:
9
+ # - Object.const_get("Foo::Bar")
10
+ # - Rails' "UserMailer".constantize (uses const_get under the hood)
11
+ # - Any metaprogramming that looks up constants by string name
12
+ #
13
+ # Note: This does NOT catch direct constant references like `Foo::Bar` in source
14
+ # code - those compile to opt_getconstant_path bytecode and bypass const_get.
15
+ # Use CoverageTracker with constant_references: true for static analysis.
16
+ #
17
+ # Register via: config.use FastCov::ConstGetTracker
18
+ # Options: root, ignored_path, threads (all default from config)
19
+ class ConstGetTracker < AbstractTracker
20
+ def install
21
+ Module.prepend(ConstGetPatch)
22
+ end
23
+
24
+ module ConstGetPatch
25
+ def const_get(name, inherit = true)
26
+ result = super
27
+ FastCov::ConstGetTracker.record do
28
+ location = self.const_source_location(name, inherit) rescue nil
29
+ location&.first
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
35
+ end
@@ -30,7 +30,7 @@ module FastCov
30
30
 
31
31
  class << self
32
32
  def record_factory_files(factory)
33
- return unless @active
33
+ return unless active
34
34
 
35
35
  definition = factory.definition
36
36
  declarations = definition.instance_variable_get(:@declarations)
@@ -43,8 +43,7 @@ module FastCov
43
43
  location = block.source_location
44
44
  next unless location
45
45
 
46
- file_path = location[0]
47
- @active.record(file_path)
46
+ record(location[0])
48
47
  end
49
48
  end
50
49
  end
@@ -15,33 +15,14 @@ module FastCov
15
15
 
16
16
  module FilePatch
17
17
  def read(name, *args, **kwargs, &block)
18
- FastCov::FileTracker.record_for_active(name)
19
- super
18
+ super.tap { FastCov::FileTracker.record { File.expand_path(name) } }
20
19
  end
21
20
 
22
21
  def open(name, *args, **kwargs, &block)
23
22
  mode = args[0]
24
23
  is_read = mode.nil? || (mode.is_a?(String) && mode.start_with?("r")) ||
25
24
  (mode.is_a?(Integer) && (mode & (File::WRONLY | File::RDWR)).zero?)
26
- FastCov::FileTracker.record_for_active(name) if is_read
27
- super
28
- end
29
- end
30
-
31
- class << self
32
- def record_for_active(path)
33
- return unless @active
34
-
35
- path_str = path.to_s
36
- return if path_str.empty?
37
-
38
- abs_path = begin
39
- File.expand_path(path_str)
40
- rescue ArgumentError, TypeError
41
- return
42
- end
43
-
44
- @active.record(abs_path)
25
+ super.tap { FastCov::FileTracker.record { File.expand_path(name) } if is_read }
45
26
  end
46
27
  end
47
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCov
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
data/lib/fast_cov.rb CHANGED
@@ -1,62 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "fast_cov/version"
4
3
  require "fast_cov/fast_cov.#{RUBY_VERSION}"
5
- require_relative "fast_cov/configuration"
6
- require_relative "fast_cov/trackers/abstract_tracker"
7
- require_relative "fast_cov/trackers/coverage_tracker"
8
- require_relative "fast_cov/trackers/file_tracker"
9
- require_relative "fast_cov/trackers/factory_bot_tracker"
10
4
 
11
5
  module FastCov
12
- class << self
13
- def configured?
14
- !@trackers.nil? && !@trackers.empty?
15
- end
16
-
17
- def configure
18
- @configuration = Configuration.new
19
- yield(@configuration)
20
- install_trackers
21
- self
22
- end
23
-
24
- def start(&block)
25
- raise "FastCov.configure must be called before start" unless configured?
26
- @trackers.each(&:start)
27
- if block
28
- result = nil
29
- begin
30
- yield
31
- ensure
32
- result = stop
33
- end
34
- result
35
- else
36
- self
37
- end
38
- end
39
-
40
- def stop
41
- raise "FastCov.configure must be called before stop" unless configured?
42
- result = Set.new
43
- @trackers.each { |t| result.merge(t.stop) }
44
- Utils.relativize_paths(result, @configuration.root)
45
- end
46
-
47
- def reset
48
- @trackers = nil
49
- @configuration = nil
50
- end
51
-
52
- private
53
-
54
- def install_trackers
55
- @trackers = @configuration.trackers.map do |entry|
56
- tracker = entry[:klass].new(@configuration, **entry[:options])
57
- tracker.install if tracker.respond_to?(:install)
58
- tracker
59
- end
60
- end
61
- end
6
+ autoload :VERSION, "fast_cov/version"
7
+ autoload :Configuration, "fast_cov/configuration"
8
+ autoload :Singleton, "fast_cov/singleton"
9
+ autoload :AbstractTracker, "fast_cov/trackers/abstract_tracker"
10
+ autoload :CoverageTracker, "fast_cov/trackers/coverage_tracker"
11
+ autoload :FileTracker, "fast_cov/trackers/file_tracker"
12
+ autoload :FactoryBotTracker, "fast_cov/trackers/factory_bot_tracker"
13
+ autoload :ConstGetTracker, "fast_cov/trackers/const_get_tracker"
14
+
15
+ extend Singleton
62
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.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ngan Pham
@@ -85,7 +85,9 @@ files:
85
85
  - lib/fast_cov/configuration.rb
86
86
  - lib/fast_cov/constant_extractor.rb
87
87
  - lib/fast_cov/dev.rb
88
+ - lib/fast_cov/singleton.rb
88
89
  - lib/fast_cov/trackers/abstract_tracker.rb
90
+ - lib/fast_cov/trackers/const_get_tracker.rb
89
91
  - lib/fast_cov/trackers/coverage_tracker.rb
90
92
  - lib/fast_cov/trackers/factory_bot_tracker.rb
91
93
  - lib/fast_cov/trackers/file_tracker.rb