ruby-coverage 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f625d52f5f7030429cd75eaa690bb45a62fa1e2a11b964e366d4ddb1c320009
4
+ data.tar.gz: ff482bc87fe61d48313e3e190333af9faabb272fb209edba68ab0ab4dd44bef0
5
+ SHA512:
6
+ metadata.gz: 34f4e2fe8a19578a25a80c9db42ec761e154726ed55b590d7350b1b143cbe0643fcf5d28037cb75b212961b0474b2218e9893f1c8c59845e62190077042ebe19
7
+ data.tar.gz: b22011f57b243cd95166f2495963386a40517a8cea439f0f5ed9057f8e8342aa9030ad5f5417aac354720df85b6a5638d6c0f50b47d2a6d6322176b537ccb672
checksums.yaml.gz.sig ADDED
Binary file
data/ext/extconf.rb ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2026, by Samuel Williams.
6
+
7
+ require "mkmf"
8
+
9
+ gem_name = File.basename(__dir__)
10
+ extension_name = "Ruby_Coverage"
11
+
12
+ append_cflags(["-Wall", "-Wno-unknown-pragmas", "-std=c99"])
13
+
14
+ if ENV.key?("RUBY_DEBUG")
15
+ $stderr.puts "Enabling debug mode..."
16
+
17
+ append_cflags(["-DRUBY_DEBUG", "-O0"])
18
+ end
19
+
20
+ $srcs = ["ruby/coverage/coverage.c", "ruby/coverage/tracer.c"]
21
+ $VPATH << "$(srcdir)/ruby/coverage"
22
+
23
+ if ENV.key?("RUBY_SANITIZE")
24
+ $stderr.puts "Enabling sanitizers..."
25
+
26
+ append_cflags(["-fsanitize=address", "-fsanitize=undefined", "-fno-omit-frame-pointer"])
27
+ $LDFLAGS << " -fsanitize=address -fsanitize=undefined"
28
+ end
29
+
30
+ have_func("rb_tracearg_instruction_sequence", "ruby/debug.h")
31
+
32
+ create_header
33
+
34
+ # Generate the makefile to compile the native binary into `ext/`:
35
+ create_makefile(extension_name)
@@ -0,0 +1,13 @@
1
+ // Released under the MIT License.
2
+ // Copyright, 2025, by Samuel Williams.
3
+
4
+ #include "coverage.h"
5
+ #include "tracer.h"
6
+
7
+ void Init_Ruby_Coverage(void)
8
+ {
9
+ VALUE Ruby = rb_define_module("Ruby");
10
+ VALUE Ruby_Coverage = rb_define_module_under(Ruby, "Coverage");
11
+
12
+ Init_Ruby_Coverage_Tracer(Ruby_Coverage);
13
+ }
@@ -0,0 +1,8 @@
1
+ // Released under the MIT License.
2
+ // Copyright, 2025, by Samuel Williams.
3
+
4
+ #pragma once
5
+
6
+ #include <ruby.h>
7
+
8
+ void Init_Ruby_Coverage(void);
@@ -0,0 +1,345 @@
1
+ // Released under the MIT License.
2
+ // Copyright, 2025, by Samuel Williams.
3
+
4
+ #include "tracer.h"
5
+
6
+ #include <ruby.h>
7
+ #include <ruby/debug.h>
8
+
9
+ static const int DEBUG = 0;
10
+
11
+ static ID id_call;
12
+ static ID id_instruction_sequence;
13
+ static ID id_path;
14
+
15
+ struct Ruby_Coverage_Tracer {
16
+ // The Ruby callback: called with (path, iseq) the first time a new file is
17
+ // compiled. iseq is the RubyVM::InstructionSequence from the script_compiled
18
+ // event. Must return an Array to use as the line-count store, or nil to
19
+ // skip the file.
20
+ VALUE callback;
21
+
22
+ // Hash: { path String => counts Array }
23
+ VALUE counts;
24
+
25
+ // Cache of the most recently seen rb_sourcefile() pointer. Stable for the
26
+ // lifetime of the current ISeq, so pointer equality is an O(1) same-file
27
+ // check without a hash lookup on every line event.
28
+ uintptr_t last_path_pointer;
29
+
30
+ // The counts Array for the file identified by last_path_pointer.
31
+ VALUE last_counts;
32
+
33
+ // Re-entrancy guard. Set while the user callback is being invoked so that
34
+ // any RUBY_EVENT_LINE events fired by the callback itself are ignored.
35
+ int in_callback;
36
+
37
+ // Used on Rubies where rb_tracearg_instruction_sequence is not part of the
38
+ // public extension headers.
39
+ VALUE script_compiled_tracepoint;
40
+ };
41
+
42
+ static void Ruby_Coverage_Tracer_mark(void *pointer)
43
+ {
44
+ struct Ruby_Coverage_Tracer *tracer = pointer;
45
+ rb_gc_mark_movable(tracer->callback);
46
+ rb_gc_mark_movable(tracer->counts);
47
+ rb_gc_mark_movable(tracer->last_counts);
48
+ rb_gc_mark_movable(tracer->script_compiled_tracepoint);
49
+ }
50
+
51
+ static void Ruby_Coverage_Tracer_free(void *pointer)
52
+ {
53
+ xfree(pointer);
54
+ }
55
+
56
+ static void Ruby_Coverage_Tracer_compact(void *pointer)
57
+ {
58
+ struct Ruby_Coverage_Tracer *tracer = pointer;
59
+ tracer->callback = rb_gc_location(tracer->callback);
60
+ tracer->counts = rb_gc_location(tracer->counts);
61
+ tracer->last_counts = rb_gc_location(tracer->last_counts);
62
+ tracer->script_compiled_tracepoint = rb_gc_location(tracer->script_compiled_tracepoint);
63
+ }
64
+
65
+ static const rb_data_type_t Ruby_Coverage_Tracer_type = {
66
+ .wrap_struct_name = "Ruby::Coverage::Tracer",
67
+ .function = {
68
+ .dmark = Ruby_Coverage_Tracer_mark,
69
+ .dfree = Ruby_Coverage_Tracer_free,
70
+ .dsize = NULL,
71
+ .dcompact = Ruby_Coverage_Tracer_compact,
72
+ },
73
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
74
+ };
75
+
76
+ struct Ruby_Coverage_Tracer_CurrentISeq {
77
+ VALUE iseq;
78
+ };
79
+
80
+ struct Ruby_Coverage_Tracer_CallbackArguments {
81
+ struct Ruby_Coverage_Tracer *tracer;
82
+ VALUE path;
83
+ VALUE iseq;
84
+ };
85
+
86
+ static VALUE Ruby_Coverage_Tracer_current_iseq_callback(const rb_debug_inspector_t *debug_inspector, void *data)
87
+ {
88
+ struct Ruby_Coverage_Tracer_CurrentISeq *current_iseq = data;
89
+ current_iseq->iseq = rb_debug_inspector_frame_iseq_get(debug_inspector, 0);
90
+
91
+ return Qnil;
92
+ }
93
+
94
+ static VALUE Ruby_Coverage_Tracer_current_iseq(void)
95
+ {
96
+ struct Ruby_Coverage_Tracer_CurrentISeq current_iseq = {.iseq = Qnil};
97
+ rb_debug_inspector_open(Ruby_Coverage_Tracer_current_iseq_callback, &current_iseq);
98
+
99
+ return current_iseq.iseq;
100
+ }
101
+
102
+ static VALUE Ruby_Coverage_Tracer_call_callback(VALUE data)
103
+ {
104
+ struct Ruby_Coverage_Tracer_CallbackArguments *arguments = (struct Ruby_Coverage_Tracer_CallbackArguments *)data;
105
+
106
+ return rb_funcall(arguments->tracer->callback, id_call, 2, arguments->path, arguments->iseq);
107
+ }
108
+
109
+ static VALUE Ruby_Coverage_Tracer_leave_callback(VALUE data)
110
+ {
111
+ struct Ruby_Coverage_Tracer_CallbackArguments *arguments = (struct Ruby_Coverage_Tracer_CallbackArguments *)data;
112
+ arguments->tracer->in_callback -= 1;
113
+
114
+ return Qnil;
115
+ }
116
+
117
+ static VALUE Ruby_Coverage_Tracer_invoke_callback(struct Ruby_Coverage_Tracer *tracer, VALUE path, VALUE iseq)
118
+ {
119
+ struct Ruby_Coverage_Tracer_CallbackArguments arguments = {
120
+ .tracer = tracer,
121
+ .path = path,
122
+ .iseq = iseq,
123
+ };
124
+
125
+ tracer->in_callback += 1;
126
+
127
+ return rb_ensure(
128
+ Ruby_Coverage_Tracer_call_callback,
129
+ (VALUE)&arguments,
130
+ Ruby_Coverage_Tracer_leave_callback,
131
+ (VALUE)&arguments
132
+ );
133
+ }
134
+
135
+ static VALUE Ruby_Coverage_Tracer_allocate(VALUE klass)
136
+ {
137
+ struct Ruby_Coverage_Tracer *tracer;
138
+ VALUE self = TypedData_Make_Struct(klass, struct Ruby_Coverage_Tracer, &Ruby_Coverage_Tracer_type, tracer);
139
+
140
+ tracer->callback = Qnil;
141
+ tracer->counts = rb_hash_new();
142
+ tracer->last_path_pointer = 0;
143
+ tracer->last_counts = Qnil;
144
+ tracer->in_callback = 0;
145
+ tracer->script_compiled_tracepoint = Qnil;
146
+
147
+ return self;
148
+ }
149
+
150
+ static VALUE Ruby_Coverage_Tracer_initialize(VALUE self)
151
+ {
152
+ struct Ruby_Coverage_Tracer *tracer;
153
+ TypedData_Get_Struct(self, struct Ruby_Coverage_Tracer,
154
+ &Ruby_Coverage_Tracer_type, tracer);
155
+
156
+ RB_OBJ_WRITE(self, &tracer->callback, rb_block_proc());
157
+
158
+ return self;
159
+ }
160
+
161
+ // Fires when any Ruby script is compiled and invokes the user callback
162
+ // immediately so the counts array is registered before the first line event.
163
+ //
164
+ // Also invalidates the rb_sourcefile() pointer cache so the line hook
165
+ // re-evaluates which counts array to use.
166
+ #ifdef HAVE_RB_TRACEARG_INSTRUCTION_SEQUENCE
167
+ // Installed via rb_add_event_hook2 with RUBY_EVENT_HOOK_FLAG_RAW_ARG.
168
+ // Uses the direct C API to extract the compiled instruction sequence.
169
+ static void Ruby_Coverage_Tracer_on_script_compiled(VALUE data, const rb_trace_arg_t *trace_arg)
170
+ {
171
+ struct Ruby_Coverage_Tracer *tracer;
172
+ TypedData_Get_Struct(data, struct Ruby_Coverage_Tracer,
173
+ &Ruby_Coverage_Tracer_type, tracer);
174
+
175
+ tracer->last_path_pointer = 0;
176
+
177
+ // Guard the entire body: rb_funcall (for #path and the user callback) can
178
+ // fire RUBY_EVENT_LINE, and without the guard on_line would run and
179
+ // potentially invoke the user callback before we've finished here.
180
+ if (tracer->in_callback) return;
181
+
182
+ VALUE iseq = rb_tracearg_instruction_sequence((rb_trace_arg_t *)trace_arg);
183
+ if (NIL_P(iseq)) { return; }
184
+
185
+ VALUE path = rb_funcall(iseq, id_path, 0);
186
+ if (NIL_P(path)) { return; }
187
+
188
+ // Reuse existing counts for re-evals of the same path so hit counts
189
+ // accumulate rather than reset.
190
+ if (!NIL_P(rb_hash_lookup(tracer->counts, path))) { return; }
191
+
192
+ VALUE counts = Ruby_Coverage_Tracer_invoke_callback(tracer, path, iseq);
193
+
194
+ if (!NIL_P(counts)) {
195
+ rb_hash_aset(tracer->counts, path, counts);
196
+ }
197
+ }
198
+ #else
199
+ // Installed via rb_tracepoint_new.
200
+ // Uses the public TracePoint object API to extract the compiled instruction
201
+ // sequence when the direct C helper is not declared in ruby/debug.h.
202
+ static void Ruby_Coverage_Tracer_on_script_compiled(VALUE tpval, void *data)
203
+ {
204
+ struct Ruby_Coverage_Tracer *tracer = data;
205
+
206
+ tracer->last_path_pointer = 0;
207
+
208
+ if (tracer->in_callback) return;
209
+
210
+ VALUE iseq = rb_funcall(tpval, id_instruction_sequence, 0);
211
+ if (NIL_P(iseq)) { return; }
212
+
213
+ VALUE path = rb_funcall(iseq, id_path, 0);
214
+ if (NIL_P(path)) { return; }
215
+
216
+ if (!NIL_P(rb_hash_lookup(tracer->counts, path))) { return; }
217
+
218
+ VALUE counts = Ruby_Coverage_Tracer_invoke_callback(tracer, path, iseq);
219
+
220
+ if (!NIL_P(counts)) {
221
+ rb_hash_aset(tracer->counts, path, counts);
222
+ }
223
+ }
224
+ #endif
225
+
226
+ // Installed via rb_add_event_hook. Fires on every new source line.
227
+ //
228
+ // Uses rb_sourcefile() pointer comparison as an O(1) same-file sentinel.
229
+ // On first entry to a new file, checks if the script_compiled hook already
230
+ // registered a counts array. If not (file was compiled before the tracer
231
+ // started), falls back to rb_profile_frames to get the iseq and invokes the
232
+ // user callback, matching the behaviour of the script_compiled path.
233
+ static void Ruby_Coverage_Tracer_on_line(rb_event_flag_t event, VALUE data, VALUE self, ID method_id, VALUE klass)
234
+ {
235
+ struct Ruby_Coverage_Tracer *tracer;
236
+ TypedData_Get_Struct(data, struct Ruby_Coverage_Tracer, &Ruby_Coverage_Tracer_type, tracer);
237
+
238
+ if (tracer->in_callback) return;
239
+
240
+ uintptr_t current_path_pointer = (uintptr_t)rb_sourcefile();
241
+
242
+ if (tracer->last_path_pointer != current_path_pointer) {
243
+ tracer->last_path_pointer = current_path_pointer;
244
+
245
+ const char *path_cstr = rb_sourcefile();
246
+ VALUE counts = Qnil;
247
+
248
+ if (path_cstr) {
249
+ VALUE path = rb_str_new_cstr(path_cstr);
250
+ counts = rb_hash_lookup(tracer->counts, path);
251
+
252
+ if (NIL_P(counts)) {
253
+ // File was compiled before the tracer started; inspect the current
254
+ // frame to recover the active instruction sequence.
255
+ VALUE iseq = Ruby_Coverage_Tracer_current_iseq();
256
+
257
+ if (!NIL_P(iseq)) {
258
+ counts = Ruby_Coverage_Tracer_invoke_callback(tracer, path, iseq);
259
+
260
+ if (!NIL_P(counts)) {
261
+ rb_hash_aset(tracer->counts, path, counts);
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ RB_OBJ_WRITE(data, &tracer->last_counts, counts);
268
+ }
269
+
270
+ if (NIL_P(tracer->last_counts)) return;
271
+
272
+ int line = rb_sourceline();
273
+
274
+ // Counts are 1-indexed: index 0 is unused (nil), index N is the hit count
275
+ // for source line N. Grow the array if necessary.
276
+ while (RARRAY_LEN(tracer->last_counts) <= line) {
277
+ rb_ary_push(tracer->last_counts, Qnil);
278
+ }
279
+
280
+ VALUE current = rb_ary_entry(tracer->last_counts, line);
281
+ rb_ary_store(tracer->last_counts, line,
282
+ NIL_P(current) ? INT2FIX(1) : INT2FIX(FIX2INT(current) + 1));
283
+ }
284
+
285
+ static VALUE Ruby_Coverage_Tracer_start(VALUE self)
286
+ {
287
+ struct Ruby_Coverage_Tracer *tracer;
288
+ TypedData_Get_Struct(self, struct Ruby_Coverage_Tracer, &Ruby_Coverage_Tracer_type, tracer);
289
+
290
+ #ifdef HAVE_RB_TRACEARG_INSTRUCTION_SEQUENCE
291
+ rb_add_event_hook2(
292
+ (rb_event_hook_func_t)Ruby_Coverage_Tracer_on_script_compiled,
293
+ RUBY_EVENT_SCRIPT_COMPILED,
294
+ self,
295
+ RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG
296
+ );
297
+ #else
298
+ if (NIL_P(tracer->script_compiled_tracepoint)) {
299
+ RB_OBJ_WRITE(self, &tracer->script_compiled_tracepoint,
300
+ rb_tracepoint_new(Qnil, RUBY_EVENT_SCRIPT_COMPILED,
301
+ Ruby_Coverage_Tracer_on_script_compiled, tracer));
302
+ }
303
+
304
+ rb_tracepoint_enable(tracer->script_compiled_tracepoint);
305
+ #endif
306
+
307
+ rb_add_event_hook(Ruby_Coverage_Tracer_on_line, RUBY_EVENT_LINE, self);
308
+
309
+ return self;
310
+ }
311
+
312
+ static VALUE Ruby_Coverage_Tracer_stop(VALUE self)
313
+ {
314
+ struct Ruby_Coverage_Tracer *tracer;
315
+ TypedData_Get_Struct(self, struct Ruby_Coverage_Tracer, &Ruby_Coverage_Tracer_type, tracer);
316
+
317
+ #ifdef HAVE_RB_TRACEARG_INSTRUCTION_SEQUENCE
318
+ rb_remove_event_hook_with_data((rb_event_hook_func_t)Ruby_Coverage_Tracer_on_script_compiled, self);
319
+ #else
320
+ if (!NIL_P(tracer->script_compiled_tracepoint)) {
321
+ rb_tracepoint_disable(tracer->script_compiled_tracepoint);
322
+ }
323
+ #endif
324
+
325
+ rb_remove_event_hook_with_data(Ruby_Coverage_Tracer_on_line, self);
326
+
327
+ tracer->last_path_pointer = 0;
328
+ RB_OBJ_WRITE(self, &tracer->last_counts, Qnil);
329
+
330
+ return self;
331
+ }
332
+
333
+ void Init_Ruby_Coverage_Tracer(VALUE Ruby_Coverage)
334
+ {
335
+ id_call = rb_intern("call");
336
+ id_instruction_sequence = rb_intern("instruction_sequence");
337
+ id_path = rb_intern("path");
338
+
339
+ VALUE Ruby_Coverage_Tracer = rb_define_class_under(Ruby_Coverage, "Tracer", rb_cObject);
340
+
341
+ rb_define_alloc_func(Ruby_Coverage_Tracer, Ruby_Coverage_Tracer_allocate);
342
+ rb_define_method(Ruby_Coverage_Tracer, "initialize", Ruby_Coverage_Tracer_initialize, 0);
343
+ rb_define_method(Ruby_Coverage_Tracer, "start", Ruby_Coverage_Tracer_start, 0);
344
+ rb_define_method(Ruby_Coverage_Tracer, "stop", Ruby_Coverage_Tracer_stop, 0);
345
+ }
@@ -0,0 +1,8 @@
1
+ // Released under the MIT License.
2
+ // Copyright, 2025, by Samuel Williams.
3
+
4
+ #pragma once
5
+
6
+ #include <ruby.h>
7
+
8
+ void Init_Ruby_Coverage_Tracer(VALUE Ruby_Coverage);
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ # @namespace
7
+ module Ruby
8
+ # @namespace
9
+ module Coverage
10
+ VERSION = "0.0.1"
11
+ end
12
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "coverage/version"
7
+
8
+ begin
9
+ require "Ruby_Coverage"
10
+ rescue LoadError => error
11
+ warn "Could not load native coverage extension: #{error}"
12
+ end
13
+
14
+ module Ruby
15
+ # A reimplementation of Ruby's built-in `Coverage` module backed by
16
+ # `rb_add_event_hook(RUBY_EVENT_LINE)` rather than ISeq counters.
17
+ #
18
+ # The key behavioural difference: re-evaluating a file under the same path
19
+ # (e.g. via `module_eval`) accumulates hit counts rather than resetting
20
+ # them, because the counter store is owned here rather than inside the ISeq.
21
+ #
22
+ # The module-level API mirrors `::Coverage` closely enough that `covered`
23
+ # can multiplex between the two, preferring this implementation when
24
+ # available and falling back to `::Coverage` otherwise.
25
+ module Coverage
26
+ @tracer = nil
27
+ @files = {}
28
+
29
+ class << self
30
+ # Walk the instruction sequence to find which lines carry a
31
+ # RUBY_EVENT_LINE event — these are the executable lines.
32
+ #
33
+ # Lines without this event (comments, `else`, `end`, blank lines)
34
+ # remain nil in the counts array, matching the nil/0 distinction
35
+ # used by Ruby's built-in Coverage module.
36
+ #
37
+ # Recurses into child ISeqs (methods, blocks, lambdas) via
38
+ # `each_child` so that all executable lines across the entire
39
+ # compilation unit are collected.
40
+ #
41
+ # @parameter iseq [RubyVM::InstructionSequence]
42
+ # @returns [Array(Integer)] Sorted, deduplicated executable line numbers.
43
+ def executable_lines(iseq)
44
+ lines = []
45
+ current_line = nil
46
+
47
+ iseq.to_a[13].each do |element|
48
+ case element
49
+ when Integer then current_line = element
50
+ when :RUBY_EVENT_LINE then lines << current_line
51
+ end
52
+ end
53
+
54
+ iseq.each_child{|child| lines.concat(executable_lines(child))}
55
+
56
+ lines.sort!
57
+ lines.uniq!
58
+ lines
59
+ end
60
+
61
+ # Start coverage tracking.
62
+ #
63
+ # The callback receives (path, iseq) for each newly compiled file and
64
+ # must return a Ruby Array to use as the line-count store for that
65
+ # file, or nil to skip it. Executable lines are pre-initialised to 0;
66
+ # non-executable lines remain nil.
67
+ #
68
+ # Safe to call multiple times; subsequent calls are no-ops.
69
+ # Returns self.
70
+ def start
71
+ return self if @tracer
72
+
73
+ @files = {}
74
+ @tracer = Tracer.new do |path, iseq|
75
+ @files[path] ||= begin
76
+ counts = []
77
+ executable_lines(iseq).each{|line| counts[line] = 0}
78
+ counts
79
+ end
80
+ end
81
+ @tracer.start
82
+
83
+ self
84
+ end
85
+
86
+ # Whether coverage is currently being tracked.
87
+ #
88
+ # @returns [Boolean]
89
+ def running?
90
+ !@tracer.nil?
91
+ end
92
+
93
+ # Return the current line-count data without stopping the tracer.
94
+ #
95
+ # The returned hash has the same shape as `::Coverage.peek_result`:
96
+ # { "/absolute/path.rb" => { lines: [nil, 0, 3, nil, ...] }, ... }
97
+ #
98
+ # @returns [Hash]
99
+ def peek_result
100
+ @files.transform_values{|counts| {lines: counts}}
101
+ end
102
+
103
+ # Return coverage results, optionally stopping or clearing the tracer.
104
+ #
105
+ # @parameter stop [Boolean] Stop tracking after returning results
106
+ # (default: true).
107
+ # @parameter clear [Boolean] Clear accumulated data and restart without
108
+ # stopping (default: false).
109
+ # @returns [Hash] Same shape as {peek_result}.
110
+ def result(stop: true, clear: false)
111
+ result = peek_result
112
+
113
+ if stop
114
+ @tracer&.stop
115
+ @tracer = nil
116
+ @files = {}
117
+ elsif clear
118
+ @tracer&.stop
119
+ @files = {}
120
+ @tracer = Tracer.new do |path, iseq|
121
+ @files[path] ||= begin
122
+ counts = []
123
+ executable_lines(iseq).each{|line| counts[line] = 0}
124
+ counts
125
+ end
126
+ end
127
+ @tracer.start
128
+ end
129
+
130
+ result
131
+ end
132
+ end
133
+ end
134
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2026, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,71 @@
1
+ # Ruby::Coverage
2
+
3
+ A native reimplementation of Ruby's built-in `Coverage` module, backed by `rb_add_event_hook(RUBY_EVENT_LINE)` rather than ISeq counters.
4
+
5
+ [![Development Status](https://github.com/socketry/ruby-coverage/workflows/Test/badge.svg)](https://github.com/socketry/ruby-coverage/actions?workflow=Test)
6
+
7
+ ## Motivation
8
+
9
+ Ruby's built-in `Coverage` module ties its counter store to the ISeq. When a file is re-evaluated under the same path — for example when a test framework loads a file into a fresh anonymous module via `module_eval` — Ruby allocates a fresh counter array and discards the previous one. Any coverage accumulated before the re-eval is lost.
10
+
11
+ `Ruby::Coverage` owns its own counter store. Re-evaluating a file under the same path simply continues incrementing the same counters. The `covered` gem can optionally use `Ruby::Coverage` in place of `::Coverage` to get correct results in test suites that load files multiple times.
12
+
13
+ ## Usage
14
+
15
+ Please see the [project documentation](https://socketry.github.io/ruby-coverage/) for more details.
16
+
17
+ - [Getting Started](https://socketry.github.io/ruby-coverage/guides/getting-started/index) - This guide explains how to use `ruby-coverage` to collect coverage for code that may be evaluated multiple times under the same path.
18
+
19
+ ### Drop-in module API
20
+
21
+ ``` ruby
22
+ require "ruby/coverage"
23
+
24
+ Ruby::Coverage.start
25
+
26
+ # ... run code ...
27
+
28
+ result = Ruby::Coverage.result
29
+ # => { "/path/to/file.rb" => { lines: [nil, 3, 1, nil, 0, ...] }, ... }
30
+ ```
31
+
32
+ ### Low-level `Tracer`
33
+
34
+ `Ruby::Coverage::Tracer` is the primitive that the module API is built on. It accepts a block that is called once each time execution enters a new file. The block receives the absolute path and must return a Ruby `Array` to use as the line-count store for that file, or `nil` to skip tracking it.
35
+
36
+ The block controls the caching strategy: returning the same `Array` for the same path accumulates counts across re-evals; returning a fresh `Array` each time gives per-ISeq isolation.
37
+
38
+ ``` ruby
39
+ files = {}
40
+
41
+ tracer = Ruby::Coverage::Tracer.new do |path|
42
+ # Accumulate across re-evals of the same path:
43
+ files[path] ||= []
44
+ end
45
+
46
+ tracer.start
47
+ # ... run tests ...
48
+ tracer.stop
49
+
50
+ # files is now populated with line-count arrays keyed by path.
51
+ ```
52
+
53
+ ## Integration with `covered`
54
+
55
+ `covered` multiplexes between `Ruby::Coverage` and `::Coverage` internally. Add `ruby-coverage` to your `Gemfile` and `covered` will prefer it automatically.
56
+
57
+ ``` ruby
58
+ # gems.rb / Gemfile
59
+ gem "ruby-coverage"
60
+ gem "covered"
61
+ ```
62
+
63
+ ## Releases
64
+
65
+ Please see the [project releases](https://socketry.github.io/ruby-coverage/releases/index) for all releases.
66
+
67
+ ### v0.0.1
68
+
69
+ ## See Also
70
+
71
+ - [covered](https://github.com/socketry/covered) — the coverage reporting gem that uses this library.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.0.1
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-coverage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
+ dependencies: []
41
+ executables: []
42
+ extensions:
43
+ - ext/extconf.rb
44
+ extra_rdoc_files: []
45
+ files:
46
+ - ext/extconf.rb
47
+ - ext/ruby/coverage/coverage.c
48
+ - ext/ruby/coverage/coverage.h
49
+ - ext/ruby/coverage/tracer.c
50
+ - ext/ruby/coverage/tracer.h
51
+ - lib/ruby/coverage.rb
52
+ - lib/ruby/coverage/version.rb
53
+ - license.md
54
+ - readme.md
55
+ - releases.md
56
+ homepage: https://github.com/socketry/ruby-coverage
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ documentation_uri: https://socketry.github.io/ruby-coverage/
61
+ source_code_uri: https://github.com/socketry/ruby-coverage.git
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '3.3'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 4.0.6
77
+ specification_version: 4
78
+ summary: A native reimplementation of Ruby's Coverage module with accumulating line
79
+ counts.
80
+ test_files: []
metadata.gz.sig ADDED
Binary file