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 +4 -4
- data/README.md +63 -228
- data/ext/fast_cov/fast_cov.c +85 -283
- data/ext/fast_cov/fast_cov.h +3 -14
- data/ext/fast_cov/fast_cov_utils.c +11 -53
- data/lib/fast_cov/benchmark/scenarios.rb +1 -33
- data/lib/fast_cov/connected_dependencies.rb +37 -0
- data/lib/fast_cov/coverage_map.rb +175 -0
- data/lib/fast_cov/trackers/abstract_tracker.rb +26 -24
- data/lib/fast_cov/trackers/const_get_tracker.rb +5 -7
- data/lib/fast_cov/trackers/factory_bot_tracker.rb +4 -5
- data/lib/fast_cov/trackers/file_tracker.rb +11 -4
- data/lib/fast_cov/version.rb +1 -1
- data/lib/fast_cov.rb +7 -10
- metadata +3 -19
- data/lib/fast_cov/configuration.rb +0 -71
- data/lib/fast_cov/constant_extractor.rb +0 -53
- data/lib/fast_cov/singleton.rb +0 -44
- data/lib/fast_cov/trackers/coverage_tracker.rb +0 -28
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastCov
|
|
4
|
+
class ConnectedDependencies
|
|
5
|
+
def initialize
|
|
6
|
+
@connections = {}
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def connect(from:, to:)
|
|
11
|
+
@mutex.synchronize do
|
|
12
|
+
(@connections[from] ||= {})[to] = true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def expand(paths)
|
|
17
|
+
raise ArgumentError, "paths must be a Set" unless paths.is_a?(Set)
|
|
18
|
+
|
|
19
|
+
pending_paths = paths.to_a
|
|
20
|
+
|
|
21
|
+
until pending_paths.empty?
|
|
22
|
+
path = pending_paths.pop
|
|
23
|
+
|
|
24
|
+
connections = @connections[path]
|
|
25
|
+
next unless connections
|
|
26
|
+
|
|
27
|
+
connections.each_key do |dependency|
|
|
28
|
+
next unless paths.add?(dependency)
|
|
29
|
+
|
|
30
|
+
pending_paths << dependency
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
paths
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module FastCov
|
|
6
|
+
class CoverageMap
|
|
7
|
+
class ConfigurationError < StandardError; end
|
|
8
|
+
|
|
9
|
+
attr_accessor :threads
|
|
10
|
+
attr_reader :ignored_paths
|
|
11
|
+
attr_reader :root
|
|
12
|
+
attr_reader :connected_dependencies
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@root = Dir.pwd
|
|
16
|
+
@threads = true
|
|
17
|
+
@ignored_paths = []
|
|
18
|
+
@connected_dependencies = ConnectedDependencies.new
|
|
19
|
+
@trackers = []
|
|
20
|
+
@native_coverage = nil
|
|
21
|
+
@started = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def root=(value)
|
|
25
|
+
return @root = nil if value.nil?
|
|
26
|
+
|
|
27
|
+
path = value.to_s
|
|
28
|
+
unless absolute_path?(path)
|
|
29
|
+
raise ConfigurationError, "root must be an absolute path, got: #{path.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@root = path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ignored_paths=(value)
|
|
36
|
+
@ignored_paths =
|
|
37
|
+
case value
|
|
38
|
+
when nil
|
|
39
|
+
[]
|
|
40
|
+
when Array
|
|
41
|
+
value.dup
|
|
42
|
+
else
|
|
43
|
+
[value]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def use(tracker_class, **options)
|
|
48
|
+
raise "CoverageMap is already started" if @started
|
|
49
|
+
raise ArgumentError, "#{tracker_class} is already registered" if @trackers.any? { |tracker| tracker.is_a?(tracker_class) }
|
|
50
|
+
|
|
51
|
+
tracker = tracker_class.new(self, **options)
|
|
52
|
+
tracker.install if tracker.respond_to?(:install)
|
|
53
|
+
@trackers << tracker
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def start
|
|
58
|
+
if @started
|
|
59
|
+
raise "CoverageMap is already started" if block_given?
|
|
60
|
+
return self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
@native_coverage = Coverage.new(
|
|
65
|
+
root: normalized_root,
|
|
66
|
+
ignored_paths: normalized_ignored_paths,
|
|
67
|
+
threads: @threads != false
|
|
68
|
+
)
|
|
69
|
+
@native_coverage.start
|
|
70
|
+
@trackers.each(&:start)
|
|
71
|
+
@started = true
|
|
72
|
+
rescue StandardError
|
|
73
|
+
cleanup_failed_start
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if block_given?
|
|
78
|
+
result = nil
|
|
79
|
+
begin
|
|
80
|
+
yield
|
|
81
|
+
ensure
|
|
82
|
+
result = stop
|
|
83
|
+
end
|
|
84
|
+
result
|
|
85
|
+
else
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stop
|
|
91
|
+
return Set.new unless @started
|
|
92
|
+
|
|
93
|
+
result = Set.new(@native_coverage.stop.each_key)
|
|
94
|
+
@trackers.reverse_each { |tracker| result.merge(tracker.stop) }
|
|
95
|
+
@connected_dependencies.expand(result)
|
|
96
|
+
Utils.relativize_paths(result, normalized_root)
|
|
97
|
+
ensure
|
|
98
|
+
@native_coverage = nil
|
|
99
|
+
@started = false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def include_path?(path)
|
|
103
|
+
return false unless path
|
|
104
|
+
return false unless Utils.path_within?(path, normalized_root)
|
|
105
|
+
return true if @ignored_paths.empty?
|
|
106
|
+
|
|
107
|
+
normalized_ignored_paths.none? { |ignored_path| Utils.path_within?(path, ignored_path) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def connect(from:, to:)
|
|
111
|
+
source = normalize_path(from)
|
|
112
|
+
target = normalize_path(to)
|
|
113
|
+
return unless include_path?(source)
|
|
114
|
+
return unless include_path?(target)
|
|
115
|
+
return if source == target
|
|
116
|
+
|
|
117
|
+
@connected_dependencies.connect(from: source, to: target)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def normalized_root
|
|
123
|
+
path = @root&.to_s
|
|
124
|
+
raise ConfigurationError, "root is required" if path.nil? || path.empty?
|
|
125
|
+
raise ConfigurationError, "root must be an absolute path, got: #{path.inspect}" unless absolute_path?(path)
|
|
126
|
+
|
|
127
|
+
path
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def normalized_ignored_paths
|
|
131
|
+
root = normalized_root
|
|
132
|
+
|
|
133
|
+
@ignored_paths.map do |value|
|
|
134
|
+
path = value.to_s
|
|
135
|
+
path = File.join(root, path) unless absolute_path?(path)
|
|
136
|
+
|
|
137
|
+
unless Utils.path_within?(path, root)
|
|
138
|
+
raise ConfigurationError,
|
|
139
|
+
"ignored_paths must be inside root (#{root.inspect}), got: #{path.inspect}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
path
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def absolute_path?(path)
|
|
147
|
+
Pathname.new(path).absolute?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def normalize_path(path)
|
|
151
|
+
return if path.nil?
|
|
152
|
+
|
|
153
|
+
File.expand_path(path.to_s)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def cleanup_failed_start
|
|
157
|
+
@trackers.reverse_each do |tracker|
|
|
158
|
+
begin
|
|
159
|
+
tracker.stop
|
|
160
|
+
rescue StandardError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
begin
|
|
166
|
+
@native_coverage&.stop
|
|
167
|
+
rescue StandardError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
@native_coverage = nil
|
|
172
|
+
@started = false
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -4,8 +4,8 @@ module FastCov
|
|
|
4
4
|
# Base class for trackers that record file paths during coverage.
|
|
5
5
|
#
|
|
6
6
|
# Provides common functionality:
|
|
7
|
-
# - Path filtering (root,
|
|
8
|
-
# - Thread-aware recording (threads
|
|
7
|
+
# - Path filtering (root, ignored_paths)
|
|
8
|
+
# - Thread-aware recording (CoverageMap#threads)
|
|
9
9
|
# - File collection and lifecycle management
|
|
10
10
|
#
|
|
11
11
|
# Descendants override hooks: on_start, on_stop, on_record
|
|
@@ -15,10 +15,8 @@ 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
|
-
def initialize(
|
|
19
|
-
@
|
|
20
|
-
@ignored_path = options.fetch(:ignored_path, config.ignored_path)
|
|
21
|
-
@threads = options.fetch(:threads, config.threads)
|
|
18
|
+
def initialize(coverage_map, **_options)
|
|
19
|
+
@coverage_map = coverage_map
|
|
22
20
|
@files = nil
|
|
23
21
|
@started_thread = nil
|
|
24
22
|
end
|
|
@@ -27,7 +25,7 @@ module FastCov
|
|
|
27
25
|
|
|
28
26
|
def start
|
|
29
27
|
@files = Set.new
|
|
30
|
-
@started_thread = Thread.current unless @threads
|
|
28
|
+
@started_thread = Thread.current unless @coverage_map.threads
|
|
31
29
|
self.class.active = self
|
|
32
30
|
on_start
|
|
33
31
|
end
|
|
@@ -41,11 +39,14 @@ module FastCov
|
|
|
41
39
|
result
|
|
42
40
|
end
|
|
43
41
|
|
|
44
|
-
def record(abs_path)
|
|
45
|
-
return if !@threads && Thread.current != @started_thread
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@
|
|
42
|
+
def record(abs_path, to: nil)
|
|
43
|
+
return if !@coverage_map.threads && Thread.current != @started_thread
|
|
44
|
+
|
|
45
|
+
path = normalize_path(abs_path)
|
|
46
|
+
return unless @coverage_map.include_path?(path)
|
|
47
|
+
|
|
48
|
+
@coverage_map.connect(from: to, to: path) if to
|
|
49
|
+
@files.add(path) if on_record(path)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# Hooks for descendants - override as needed
|
|
@@ -54,26 +55,27 @@ module FastCov
|
|
|
54
55
|
def on_start; end
|
|
55
56
|
def on_stop; end
|
|
56
57
|
|
|
57
|
-
def on_record(
|
|
58
|
+
def on_record(path)
|
|
58
59
|
true
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def normalize_path(path)
|
|
65
|
+
return if path.nil?
|
|
66
|
+
|
|
67
|
+
File.expand_path(path.to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
61
70
|
class << self
|
|
62
71
|
attr_accessor :active
|
|
63
72
|
|
|
64
|
-
|
|
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
|
+
def record(to: nil)
|
|
73
74
|
return unless active
|
|
75
|
+
return unless block_given?
|
|
74
76
|
|
|
75
|
-
path =
|
|
76
|
-
active.record(path) if path
|
|
77
|
+
path = yield
|
|
78
|
+
active.record(path, to: to) if path
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def reset
|
|
@@ -12,22 +12,20 @@ module FastCov
|
|
|
12
12
|
#
|
|
13
13
|
# Note: This does NOT catch direct constant references like `Foo::Bar` in source
|
|
14
14
|
# code - those compile to opt_getconstant_path bytecode and bypass const_get.
|
|
15
|
-
# Use CoverageTracker with constant_references: true for static analysis.
|
|
16
15
|
#
|
|
17
|
-
# Register via:
|
|
18
|
-
# Options: root, ignored_path, threads (all default from config)
|
|
16
|
+
# Register via: coverage_map.use(FastCov::ConstGetTracker)
|
|
19
17
|
class ConstGetTracker < AbstractTracker
|
|
20
18
|
def install
|
|
19
|
+
return if Module.ancestors.include?(ConstGetPatch)
|
|
20
|
+
|
|
21
21
|
Module.prepend(ConstGetPatch)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
module ConstGetPatch
|
|
25
25
|
def const_get(name, inherit = true)
|
|
26
|
+
source = caller_locations(1, 1).first&.absolute_path
|
|
26
27
|
result = super
|
|
27
|
-
FastCov::ConstGetTracker.record
|
|
28
|
-
location = self.const_source_location(name, inherit) rescue nil
|
|
29
|
-
location&.first
|
|
30
|
-
end
|
|
28
|
+
FastCov::ConstGetTracker.record(to: source) { const_source_location(name, inherit)&.first }
|
|
31
29
|
result
|
|
32
30
|
end
|
|
33
31
|
end
|
|
@@ -9,14 +9,15 @@ module FastCov
|
|
|
9
9
|
# This tracker intercepts FactoryBot.factories.find (called by create/build)
|
|
10
10
|
# to record the source file where each factory was defined.
|
|
11
11
|
#
|
|
12
|
-
# Register via:
|
|
13
|
-
# Options: root, ignored_path, threads (all default from config)
|
|
12
|
+
# Register via: coverage_map.use(FastCov::FactoryBotTracker)
|
|
14
13
|
class FactoryBotTracker < AbstractTracker
|
|
15
14
|
def install
|
|
16
15
|
unless defined?(::FactoryBot)
|
|
17
16
|
raise LoadError, "FactoryBotTracker requires the factory_bot gem to be installed"
|
|
18
17
|
end
|
|
19
18
|
|
|
19
|
+
return if ::FactoryBot.factories.singleton_class.ancestors.include?(RegistryPatch)
|
|
20
|
+
|
|
20
21
|
::FactoryBot.factories.singleton_class.prepend(RegistryPatch)
|
|
21
22
|
end
|
|
22
23
|
|
|
@@ -41,9 +42,7 @@ module FastCov
|
|
|
41
42
|
next unless block.is_a?(Proc)
|
|
42
43
|
|
|
43
44
|
location = block.source_location
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
record(location[0])
|
|
45
|
+
record { location&.first }
|
|
47
46
|
end
|
|
48
47
|
end
|
|
49
48
|
end
|
|
@@ -6,23 +6,30 @@ module FastCov
|
|
|
6
6
|
# Tracks files read from disk during coverage (JSON, YAML, .rb templates, etc.)
|
|
7
7
|
# via File.read and File.open.
|
|
8
8
|
#
|
|
9
|
-
# Register via:
|
|
10
|
-
# Options: root, ignored_path, threads (all default from config)
|
|
9
|
+
# Register via: coverage_map.use(FastCov::FileTracker)
|
|
11
10
|
class FileTracker < AbstractTracker
|
|
12
11
|
def install
|
|
12
|
+
return if File.singleton_class.ancestors.include?(FilePatch)
|
|
13
|
+
|
|
13
14
|
File.singleton_class.prepend(FilePatch)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
module FilePatch
|
|
17
18
|
def read(name, *args, **kwargs, &block)
|
|
18
|
-
|
|
19
|
+
source = caller_locations(1, 1).first&.absolute_path
|
|
20
|
+
super.tap do
|
|
21
|
+
FastCov::FileTracker.record(to: source) { File.expand_path(name) }
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def open(name, *args, **kwargs, &block)
|
|
22
26
|
mode = args[0]
|
|
23
27
|
is_read = mode.nil? || (mode.is_a?(String) && mode.start_with?("r")) ||
|
|
24
28
|
(mode.is_a?(Integer) && (mode & (File::WRONLY | File::RDWR)).zero?)
|
|
25
|
-
|
|
29
|
+
source = caller_locations(1, 1).first&.absolute_path
|
|
30
|
+
super.tap do
|
|
31
|
+
FastCov::FileTracker.record(to: source) { File.expand_path(name) } if is_read
|
|
32
|
+
end
|
|
26
33
|
end
|
|
27
34
|
end
|
|
28
35
|
end
|
data/lib/fast_cov/version.rb
CHANGED
data/lib/fast_cov.rb
CHANGED
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
require "fast_cov/fast_cov.#{RUBY_VERSION}"
|
|
4
4
|
|
|
5
5
|
module FastCov
|
|
6
|
-
autoload :VERSION,
|
|
7
|
-
autoload :
|
|
8
|
-
autoload :
|
|
9
|
-
autoload :AbstractTracker,
|
|
10
|
-
autoload :
|
|
11
|
-
autoload :
|
|
12
|
-
autoload :
|
|
13
|
-
autoload :ConstGetTracker, "fast_cov/trackers/const_get_tracker"
|
|
14
|
-
|
|
15
|
-
extend Singleton
|
|
6
|
+
autoload :VERSION, File.expand_path("fast_cov/version", __dir__)
|
|
7
|
+
autoload :ConnectedDependencies, File.expand_path("fast_cov/connected_dependencies", __dir__)
|
|
8
|
+
autoload :CoverageMap, File.expand_path("fast_cov/coverage_map", __dir__)
|
|
9
|
+
autoload :AbstractTracker, File.expand_path("fast_cov/trackers/abstract_tracker", __dir__)
|
|
10
|
+
autoload :FileTracker, File.expand_path("fast_cov/trackers/file_tracker", __dir__)
|
|
11
|
+
autoload :FactoryBotTracker, File.expand_path("fast_cov/trackers/factory_bot_tracker", __dir__)
|
|
12
|
+
autoload :ConstGetTracker, File.expand_path("fast_cov/trackers/const_get_tracker", __dir__)
|
|
16
13
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ngan Pham
|
|
@@ -9,20 +9,6 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: prism
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '1.0'
|
|
26
12
|
- !ruby/object:Gem::Dependency
|
|
27
13
|
name: rake
|
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -96,13 +82,11 @@ files:
|
|
|
96
82
|
- lib/fast_cov/benchmark/runner.rb
|
|
97
83
|
- lib/fast_cov/benchmark/scenarios.rb
|
|
98
84
|
- lib/fast_cov/compiler.rb
|
|
99
|
-
- lib/fast_cov/
|
|
100
|
-
- lib/fast_cov/
|
|
85
|
+
- lib/fast_cov/connected_dependencies.rb
|
|
86
|
+
- lib/fast_cov/coverage_map.rb
|
|
101
87
|
- lib/fast_cov/dev.rb
|
|
102
|
-
- lib/fast_cov/singleton.rb
|
|
103
88
|
- lib/fast_cov/trackers/abstract_tracker.rb
|
|
104
89
|
- lib/fast_cov/trackers/const_get_tracker.rb
|
|
105
|
-
- lib/fast_cov/trackers/coverage_tracker.rb
|
|
106
90
|
- lib/fast_cov/trackers/factory_bot_tracker.rb
|
|
107
91
|
- lib/fast_cov/trackers/file_tracker.rb
|
|
108
92
|
- lib/fast_cov/version.rb
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "pathname"
|
|
4
|
-
|
|
5
|
-
module FastCov
|
|
6
|
-
class Configuration
|
|
7
|
-
class ConfigurationError < StandardError; end
|
|
8
|
-
|
|
9
|
-
attr_accessor :threads
|
|
10
|
-
attr_reader :root, :ignored_path
|
|
11
|
-
|
|
12
|
-
def initialize
|
|
13
|
-
@root = Dir.pwd
|
|
14
|
-
@ignored_path = nil
|
|
15
|
-
@threads = true
|
|
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
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def use(tracker_class, **options)
|
|
49
|
-
@tracker_definitions << {klass: tracker_class, options: options}
|
|
50
|
-
end
|
|
51
|
-
|
|
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
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def reset
|
|
61
|
-
initialize
|
|
62
|
-
self
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def absolute_path?(path)
|
|
68
|
-
Pathname.new(path).absolute?
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "prism"
|
|
4
|
-
|
|
5
|
-
module FastCov
|
|
6
|
-
module ConstantExtractor
|
|
7
|
-
def self.extract(filename)
|
|
8
|
-
result = Prism.parse_file(filename)
|
|
9
|
-
constants = []
|
|
10
|
-
collect_constants(result.value, constants)
|
|
11
|
-
constants
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class << self
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def collect_constants(node, constants)
|
|
18
|
-
case node
|
|
19
|
-
when Prism::ConstantPathNode
|
|
20
|
-
path = resolve_constant_path(node)
|
|
21
|
-
if path
|
|
22
|
-
constants << path
|
|
23
|
-
return
|
|
24
|
-
end
|
|
25
|
-
# Dynamic parent (e.g., expr::Foo) — fall through to walk children
|
|
26
|
-
when Prism::ConstantReadNode
|
|
27
|
-
constants << node.name.to_s
|
|
28
|
-
return
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
node.compact_child_nodes.each { |child| collect_constants(child, constants) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def resolve_constant_path(node)
|
|
35
|
-
parts = []
|
|
36
|
-
current = node
|
|
37
|
-
|
|
38
|
-
while current.is_a?(Prism::ConstantPathNode)
|
|
39
|
-
parts.unshift(current.name.to_s)
|
|
40
|
-
current = current.parent
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
if current.is_a?(Prism::ConstantReadNode)
|
|
44
|
-
parts.unshift(current.name.to_s)
|
|
45
|
-
elsif !current.nil?
|
|
46
|
-
return nil
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
parts.join("::")
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
data/lib/fast_cov/singleton.rb
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
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
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FastCov
|
|
4
|
-
# Wraps the FastCov::Coverage C extension as a tracker plugin.
|
|
5
|
-
# Handles line coverage, allocation tracing, and constant resolution.
|
|
6
|
-
#
|
|
7
|
-
# Register via: config.use FastCov::CoverageTracker
|
|
8
|
-
# Options: root, ignored_path, threads, constant_references, allocations
|
|
9
|
-
class CoverageTracker
|
|
10
|
-
def initialize(config, **options)
|
|
11
|
-
@coverage = Coverage.new(
|
|
12
|
-
root: options.fetch(:root, config.root),
|
|
13
|
-
ignored_path: options.fetch(:ignored_path, config.ignored_path),
|
|
14
|
-
threads: options.fetch(:threads, config.threads),
|
|
15
|
-
constant_references: options.fetch(:constant_references, true),
|
|
16
|
-
allocations: options.fetch(:allocations, true)
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def start
|
|
21
|
-
@coverage.start
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def stop
|
|
25
|
-
Set.new(@coverage.stop.each_key)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|