Skip to content

Commit af90cee

Browse files
committed
+ coverage compat
1 parent 991e6ce commit af90cee

9 files changed

Lines changed: 140 additions & 2 deletions

File tree

‎Gemfile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ source "https://rubygems.org"
44

55
gem "debug", platform: :mri
66
gem "bootsnap", platform: [:mri, :truffleruby]
7+
gem "simplecov"
78

89
gemspec
910

‎lib/require-hooks/api.rb‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def initialize(patterns: nil, exclude_patterns: nil)
1414
@hijack_load = []
1515

1616
@empty = nil
17+
@readonly = nil
1718
end
1819

1920
def to_key
@@ -31,6 +32,12 @@ def empty?
3132
@empty = @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
3233
end
3334

35+
def readonly?
36+
return @readonly unless @readonly.nil?
37+
38+
@readonly = @source_transform.empty? && @hijack_load.empty?
39+
end
40+
3441
def source_transform?
3542
@source_transform.any?
3643
end
@@ -162,5 +169,25 @@ def context_for(path)
162169

163170
ctx
164171
end
172+
173+
# Hack to enable coverage for hooked files.
174+
# Requires eval coverage to be on.
175+
def setup_path_coverage(path, contents = nil)
176+
return unless defined?(Coverage) && Coverage.running?
177+
178+
return unless eval_coverage_enabled?
179+
180+
Kernel.eval("\n" * (contents || File.read(path)).lines.size, TOPLEVEL_BINDING, path, 1) # rubocop:disable Style/EvalWithLocation,Security/Eval
181+
end
182+
183+
private
184+
185+
def eval_coverage_enabled?
186+
return @eval_coverage_enabled if defined?(@eval_coverage_enabled)
187+
probe_path = File.join(__dir__, "coverage_probe.rb")
188+
Kernel.eval("proc { |val| val }", TOPLEVEL_BINDING, probe_path, 1) # rubocop:disable Style/EvalWithLocation
189+
190+
@eval_coverage_enabled = Coverage.peek_result.key?(probe_path)
191+
end
165192
end
166193
end

‎lib/require-hooks/mode/bootsnap.rb‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,29 @@ def load_iseq(path)
3535

3636
ctx.run_around_load_callbacks(path) do
3737
iseq = super
38-
return unless iseq
38+
# Bootsnap returns nil when the coverage is on,
39+
unless iseq
40+
next unless defined?(Coverage) && Coverage.running?
41+
42+
iseq =
43+
if ctx.source_transform? || ctx.hijack?
44+
new_contents = ctx.perform_source_transform(path)
45+
46+
RequireHooks.setup_path_coverage(path, new_contents)
47+
48+
hijacked = ctx.try_hijack_load(path, new_contents)
49+
50+
if hijacked
51+
raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
52+
hijacked
53+
elsif new_contents
54+
RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
55+
end
56+
else
57+
RequireHooks.setup_path_coverage(path)
58+
RubyVM::InstructionSequence.compile_file(path)
59+
end
60+
end
3961

4062
iseq.eval
4163
EMPTY_ISEQ

‎lib/require-hooks/mode/kernel_patch.rb‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ def load(path, ctx: nil)
1212
next load_without_require_hooks(path) unless ctx.source_transform? || ctx.hijack?
1313

1414
new_contents = ctx.perform_source_transform(path)
15+
1516
hijacked = ctx.try_hijack_load(path, new_contents)
1617

17-
return try_evaluate(path, hijacked) if hijacked
18+
if hijacked
19+
RequireHooks.setup_path_coverage(path, new_contents)
20+
21+
return try_evaluate(path, hijacked)
22+
end
1823

1924
if new_contents
25+
RequireHooks.setup_path_coverage(path, new_contents)
26+
2027
evaluate(new_contents, path)
2128
true
2229
else

‎lib/require-hooks/mode/load_iseq.rb‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ def load_iseq(path)
1414
iseq =
1515
if ctx.source_transform? || ctx.hijack?
1616
new_contents = ctx.perform_source_transform(path)
17+
18+
RequireHooks.setup_path_coverage(path, new_contents)
19+
1720
hijacked = ctx.try_hijack_load(path, new_contents)
1821

1922
if hijacked
@@ -24,7 +27,10 @@ def load_iseq(path)
2427
end
2528
end
2629

30+
RequireHooks.setup_path_coverage(path)
31+
2732
iseq ||= (defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path))
33+
2834
iseq.eval
2935
EMPTY_ISEQ
3036
end

‎spec/require-hooks/bootsnap_spec.rb‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,19 @@
5050
err.should include("bootsnap-syntax-error.rb:1")
5151
end
5252
end
53+
54+
context "coverage" do
55+
# Eval coverage is only avaiable from 3.2
56+
next unless RUBY_VERSION >= "3.2.0"
57+
58+
it "does not break coverage tracking" do
59+
run_ruby(
60+
File.join(__dir__, "fixtures", "coverage.rb").to_s,
61+
env: {"BOOTSNAP" => "1"}
62+
) do |_status, output, _err|
63+
output.should include("./hello.rb: [")
64+
output.should include("./coverable.rb: [1, 1, 1, nil, nil, nil, 1]\n")
65+
end
66+
end
67+
end
5368
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
require "fileutils"
4+
require_relative "../support/command_testing"
5+
6+
describe "require-hooks vs coverage" do
7+
# Eval coverage is only avaiable from 3.2
8+
next unless RUBY_VERSION >= "3.2.0"
9+
# Truffle doesn't support it either
10+
next if defined?(TruffleRuby)
11+
12+
it "does not break coverage tracking" do
13+
run_ruby(
14+
File.join(__dir__, "fixtures", "coverage.rb").to_s
15+
) do |_status, output, _err|
16+
output.should include("./hello.rb: [")
17+
output.should include("./coverable.rb: [1, 1, 1, nil, nil, nil, 1]\n")
18+
end
19+
end
20+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Coverable
2+
def self.call
3+
"cover up"
4+
end
5+
end
6+
7+
Coverable.call
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
$events = []
4+
5+
if ENV["BOOTSNAP"]
6+
require "bootsnap"
7+
Bootsnap.setup(
8+
cache_dir: File.join(__dir__, "tmp/cache"),
9+
development_mode: true,
10+
load_path_cache: true,
11+
compile_cache_iseq: true,
12+
compile_cache_yaml: true
13+
)
14+
end
15+
16+
require "simplecov"
17+
# Prevent the coverage/ creation
18+
SimpleCov.at_exit {}
19+
SimpleCov.start do
20+
enable_coverage_for_eval
21+
end
22+
23+
require "require-hooks/setup"
24+
25+
RequireHooks.around_load(patterns: ["*/fixtures/coverable.rb"]) do |path, &block|
26+
$events << "before-hook"
27+
block.call.tap { $events << "after-hook" }
28+
end
29+
30+
load File.join(__dir__, "hello.rb")
31+
load File.join(__dir__, "coverable.rb")
32+
33+
puts Coverage.peek_result.map { |k, v| "#{k.gsub(__dir__, ".")}: #{v[:lines].inspect}" }.join("\n")

0 commit comments

Comments
 (0)