fast_cov 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +339 -0
- data/ext/fast_cov/extconf.rb +12 -0
- data/ext/fast_cov/fast_cov.c +615 -0
- data/ext/fast_cov/fast_cov.h +32 -0
- data/ext/fast_cov/fast_cov_utils.c +104 -0
- data/lib/fast_cov/benchmark/runner.rb +147 -0
- data/lib/fast_cov/benchmark/scenarios.rb +103 -0
- data/lib/fast_cov/compiler.rb +56 -0
- data/lib/fast_cov/configuration.rb +27 -0
- data/lib/fast_cov/constant_extractor.rb +53 -0
- data/lib/fast_cov/dev.rb +22 -0
- data/lib/fast_cov/trackers/abstract_tracker.rb +73 -0
- data/lib/fast_cov/trackers/coverage_tracker.rb +28 -0
- data/lib/fast_cov/trackers/factory_bot_tracker.rb +52 -0
- data/lib/fast_cov/trackers/file_tracker.rb +48 -0
- data/lib/fast_cov/version.rb +5 -0
- data/lib/fast_cov.rb +62 -0
- metadata +117 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/version.h>
|
|
3
|
+
#include <ruby/debug.h>
|
|
4
|
+
#include <ruby/st.h>
|
|
5
|
+
|
|
6
|
+
#include <stdbool.h>
|
|
7
|
+
|
|
8
|
+
#include "fast_cov.h"
|
|
9
|
+
|
|
10
|
+
// FastCov: native C extension for fast Ruby code coverage tracking.
|
|
11
|
+
//
|
|
12
|
+
// Tracks which source files are executed during a test run by hooking into
|
|
13
|
+
// Ruby VM events. Designed for test impact analysis.
|
|
14
|
+
|
|
15
|
+
#define MAX_CONST_RESOLUTION_ROUNDS 10
|
|
16
|
+
|
|
17
|
+
// threads: true = multi-threaded (global hook), false = single-threaded (per-thread hook)
|
|
18
|
+
|
|
19
|
+
// Constant resolution via Ruby helper (FastCov::ConstantExtractor)
|
|
20
|
+
static VALUE cConstantExtractor;
|
|
21
|
+
static ID id_extract;
|
|
22
|
+
static ID id_keys;
|
|
23
|
+
|
|
24
|
+
// Cache infrastructure
|
|
25
|
+
VALUE fast_cov_cache_hash; // process-level cache (non-static for access from utils)
|
|
26
|
+
static VALUE cDigest; // Digest::MD5
|
|
27
|
+
static ID id_file;
|
|
28
|
+
static ID id_hexdigest;
|
|
29
|
+
static ID id_clear;
|
|
30
|
+
static ID id_merge_bang;
|
|
31
|
+
|
|
32
|
+
// Forward declarations
|
|
33
|
+
static VALUE fast_cov_stop(VALUE self);
|
|
34
|
+
|
|
35
|
+
static int mark_key_for_gc_i(st_data_t key, st_data_t _value,
|
|
36
|
+
st_data_t _data) {
|
|
37
|
+
rb_gc_mark((VALUE)key);
|
|
38
|
+
return ST_CONTINUE;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- Data structure -----------------------------------------------------
|
|
42
|
+
|
|
43
|
+
struct fast_cov_data {
|
|
44
|
+
VALUE impacted_files;
|
|
45
|
+
|
|
46
|
+
char *root;
|
|
47
|
+
long root_len;
|
|
48
|
+
|
|
49
|
+
char *ignored_path;
|
|
50
|
+
long ignored_path_len;
|
|
51
|
+
|
|
52
|
+
uintptr_t last_filename_ptr;
|
|
53
|
+
|
|
54
|
+
bool threads;
|
|
55
|
+
bool constant_references;
|
|
56
|
+
bool allocations;
|
|
57
|
+
VALUE th_covered;
|
|
58
|
+
|
|
59
|
+
VALUE object_allocation_tracepoint;
|
|
60
|
+
st_table *klasses_table;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---- GC callbacks -------------------------------------------------------
|
|
64
|
+
//
|
|
65
|
+
// We use rb_gc_mark (non-movable, pins objects) instead of rb_gc_mark_movable.
|
|
66
|
+
// On Ruby 3.4, rb_gc_mark_movable + dcompact causes T_NONE crashes during
|
|
67
|
+
// compaction. Pinning avoids this with negligible performance impact.
|
|
68
|
+
|
|
69
|
+
static void fast_cov_mark(void *ptr) {
|
|
70
|
+
struct fast_cov_data *data = ptr;
|
|
71
|
+
rb_gc_mark(data->impacted_files);
|
|
72
|
+
rb_gc_mark(data->th_covered);
|
|
73
|
+
rb_gc_mark(data->object_allocation_tracepoint);
|
|
74
|
+
|
|
75
|
+
if (data->klasses_table != NULL) {
|
|
76
|
+
st_foreach(data->klasses_table, mark_key_for_gc_i, 0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static void fast_cov_free(void *ptr) {
|
|
81
|
+
struct fast_cov_data *data = ptr;
|
|
82
|
+
if (data->root) xfree(data->root);
|
|
83
|
+
if (data->ignored_path) xfree(data->ignored_path);
|
|
84
|
+
if (data->klasses_table) st_free_table(data->klasses_table);
|
|
85
|
+
xfree(data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static const rb_data_type_t fast_cov_data_type = {
|
|
89
|
+
.wrap_struct_name = "fast_cov",
|
|
90
|
+
.function = {.dmark = fast_cov_mark,
|
|
91
|
+
.dfree = fast_cov_free,
|
|
92
|
+
.dsize = NULL},
|
|
93
|
+
.flags = 0};
|
|
94
|
+
|
|
95
|
+
// ---- Allocator ----------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
static VALUE fast_cov_allocate(VALUE klass) {
|
|
98
|
+
struct fast_cov_data *data;
|
|
99
|
+
VALUE obj = TypedData_Make_Struct(klass, struct fast_cov_data,
|
|
100
|
+
&fast_cov_data_type, data);
|
|
101
|
+
|
|
102
|
+
// Initialize all VALUE fields to Qnil before any allocation that could
|
|
103
|
+
// trigger GC. TypedData_Make_Struct zeroes memory (via calloc), but 0 is
|
|
104
|
+
// Qfalse, not Qnil — and marking Qfalse can confuse Ruby 3.4's GC.
|
|
105
|
+
data->impacted_files = Qnil;
|
|
106
|
+
data->th_covered = Qnil;
|
|
107
|
+
data->object_allocation_tracepoint = Qnil;
|
|
108
|
+
data->klasses_table = NULL;
|
|
109
|
+
|
|
110
|
+
data->impacted_files = rb_hash_new();
|
|
111
|
+
data->root = NULL;
|
|
112
|
+
data->root_len = 0;
|
|
113
|
+
data->ignored_path = NULL;
|
|
114
|
+
data->ignored_path_len = 0;
|
|
115
|
+
data->last_filename_ptr = 0;
|
|
116
|
+
data->threads = true;
|
|
117
|
+
data->constant_references = true;
|
|
118
|
+
data->allocations = true;
|
|
119
|
+
data->klasses_table = st_init_numtable();
|
|
120
|
+
|
|
121
|
+
return obj;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- Internal helpers ---------------------------------------------------
|
|
125
|
+
|
|
126
|
+
static bool record_impacted_file(struct fast_cov_data *data, VALUE filename) {
|
|
127
|
+
if (!fast_cov_is_path_included(RSTRING_PTR(filename), data->root,
|
|
128
|
+
data->root_len, data->ignored_path,
|
|
129
|
+
data->ignored_path_len)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
rb_hash_aset(data->impacted_files, filename, Qtrue);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- Line event callback ------------------------------------------------
|
|
138
|
+
|
|
139
|
+
static void on_line_event(rb_event_flag_t event, VALUE self_data, VALUE self,
|
|
140
|
+
ID id, VALUE klass) {
|
|
141
|
+
struct fast_cov_data *data;
|
|
142
|
+
TypedData_Get_Struct(self_data, struct fast_cov_data, &fast_cov_data_type,
|
|
143
|
+
data);
|
|
144
|
+
|
|
145
|
+
const char *c_filename = rb_sourcefile();
|
|
146
|
+
|
|
147
|
+
uintptr_t current_filename_ptr = (uintptr_t)c_filename;
|
|
148
|
+
if (data->last_filename_ptr == current_filename_ptr) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
data->last_filename_ptr = current_filename_ptr;
|
|
152
|
+
|
|
153
|
+
VALUE top_frame;
|
|
154
|
+
if (rb_profile_frames(0, 1, &top_frame, NULL) != 1) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
VALUE filename = rb_profile_frame_path(top_frame);
|
|
159
|
+
if (filename == Qnil) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
record_impacted_file(data, filename);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- Allocation tracing helpers -----------------------------------------
|
|
167
|
+
|
|
168
|
+
static bool record_impacted_klass(struct fast_cov_data *data, VALUE klass) {
|
|
169
|
+
VALUE klass_name = fast_cov_rescue_nil(rb_class_name, klass);
|
|
170
|
+
if (klass_name == Qnil) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
VALUE filename = fast_cov_resolve_const_to_file(klass_name);
|
|
175
|
+
if (filename == Qnil) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return record_impacted_file(data, filename);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static int each_instantiated_klass(st_data_t key, st_data_t _value,
|
|
183
|
+
st_data_t cb_data) {
|
|
184
|
+
VALUE klass = (VALUE)key;
|
|
185
|
+
struct fast_cov_data *data = (struct fast_cov_data *)cb_data;
|
|
186
|
+
|
|
187
|
+
VALUE ancestors = fast_cov_rescue_nil(rb_mod_ancestors, klass);
|
|
188
|
+
if (ancestors == Qnil || !RB_TYPE_P(ancestors, T_ARRAY)) {
|
|
189
|
+
return ST_CONTINUE;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
long len = RARRAY_LEN(ancestors);
|
|
193
|
+
for (long i = 0; i < len; i++) {
|
|
194
|
+
VALUE mod = rb_ary_entry(ancestors, i);
|
|
195
|
+
if (mod == Qnil) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
record_impacted_klass(data, mod);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return ST_CONTINUE;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- Newobj event callback ----------------------------------------------
|
|
205
|
+
|
|
206
|
+
static void on_newobj_event(VALUE tracepoint_data, void *raw_data) {
|
|
207
|
+
rb_trace_arg_t *tracearg = rb_tracearg_from_tracepoint(tracepoint_data);
|
|
208
|
+
VALUE new_object = rb_tracearg_object(tracearg);
|
|
209
|
+
|
|
210
|
+
enum ruby_value_type type = rb_type(new_object);
|
|
211
|
+
if (type != RUBY_T_OBJECT && type != RUBY_T_STRUCT) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
VALUE klass = rb_class_of(new_object);
|
|
216
|
+
#if RUBY_API_VERSION_MAJOR < 4
|
|
217
|
+
// Ruby 3.x: rb_class_of can return 0 during NEWOBJ for some allocations
|
|
218
|
+
if (klass == Qnil || klass == 0) {
|
|
219
|
+
#else
|
|
220
|
+
if (klass == Qnil) {
|
|
221
|
+
#endif
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (rb_mod_name(klass) == Qnil) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
struct fast_cov_data *data = (struct fast_cov_data *)raw_data;
|
|
229
|
+
st_insert(data->klasses_table, (st_data_t)klass, 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- Constant reference resolution (cached) -----------------------------
|
|
233
|
+
|
|
234
|
+
// Computes MD5 hexdigest of a file's contents.
|
|
235
|
+
static VALUE compute_file_digest_body(VALUE filename) {
|
|
236
|
+
VALUE digest_obj = rb_funcall(cDigest, id_file, 1, filename);
|
|
237
|
+
return rb_funcall(digest_obj, id_hexdigest, 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static VALUE compute_file_digest(VALUE filename) {
|
|
241
|
+
int exception_state;
|
|
242
|
+
VALUE result =
|
|
243
|
+
rb_protect(compute_file_digest_body, filename, &exception_state);
|
|
244
|
+
if (exception_state != 0) {
|
|
245
|
+
rb_set_errinfo(Qnil);
|
|
246
|
+
return Qnil;
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Parse file with Prism and extract constant names.
|
|
252
|
+
static VALUE extract_const_names_body(VALUE filename) {
|
|
253
|
+
return rb_funcall(cConstantExtractor, id_extract, 1, filename);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Returns an array of constant name strings for a file, using the cache.
|
|
257
|
+
static VALUE get_const_refs_for_file(VALUE filename) {
|
|
258
|
+
VALUE const_refs_hash =
|
|
259
|
+
rb_hash_lookup(fast_cov_cache_hash, ID2SYM(rb_intern("const_refs")));
|
|
260
|
+
|
|
261
|
+
VALUE cached_entry = rb_hash_lookup(const_refs_hash, filename);
|
|
262
|
+
|
|
263
|
+
VALUE current_digest = compute_file_digest(filename);
|
|
264
|
+
if (NIL_P(current_digest)) {
|
|
265
|
+
if (!NIL_P(cached_entry)) {
|
|
266
|
+
rb_hash_delete(const_refs_hash, filename);
|
|
267
|
+
}
|
|
268
|
+
return Qnil;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Cache hit: digest matches
|
|
272
|
+
if (!NIL_P(cached_entry) && RB_TYPE_P(cached_entry, T_HASH)) {
|
|
273
|
+
VALUE cached_digest =
|
|
274
|
+
rb_hash_lookup(cached_entry, ID2SYM(rb_intern("digest")));
|
|
275
|
+
|
|
276
|
+
if (!NIL_P(cached_digest) &&
|
|
277
|
+
rb_str_equal(cached_digest, current_digest) == Qtrue) {
|
|
278
|
+
return rb_hash_lookup(cached_entry, ID2SYM(rb_intern("refs")));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Cache miss: parse with Prism and extract constant names
|
|
283
|
+
int exception_state;
|
|
284
|
+
VALUE const_names =
|
|
285
|
+
rb_protect(extract_const_names_body, filename, &exception_state);
|
|
286
|
+
if (exception_state != 0) {
|
|
287
|
+
rb_set_errinfo(Qnil);
|
|
288
|
+
if (!NIL_P(cached_entry)) {
|
|
289
|
+
rb_hash_delete(const_refs_hash, filename);
|
|
290
|
+
}
|
|
291
|
+
return Qnil;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Store in cache
|
|
295
|
+
VALUE new_entry = rb_hash_new();
|
|
296
|
+
rb_hash_aset(new_entry, ID2SYM(rb_intern("digest")), current_digest);
|
|
297
|
+
rb_hash_aset(new_entry, ID2SYM(rb_intern("refs")), const_names);
|
|
298
|
+
rb_hash_aset(const_refs_hash, filename, new_entry);
|
|
299
|
+
|
|
300
|
+
return const_names;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
static void resolve_constant_references(struct fast_cov_data *data) {
|
|
304
|
+
VALUE seen_consts = rb_hash_new();
|
|
305
|
+
VALUE processed_files = rb_hash_new();
|
|
306
|
+
|
|
307
|
+
for (int round = 0; round < MAX_CONST_RESOLUTION_ROUNDS; round++) {
|
|
308
|
+
VALUE keys = rb_funcall(data->impacted_files, id_keys, 0);
|
|
309
|
+
long num_keys = RARRAY_LEN(keys);
|
|
310
|
+
int found_new_file = 0;
|
|
311
|
+
|
|
312
|
+
for (long i = 0; i < num_keys; i++) {
|
|
313
|
+
VALUE filename = rb_ary_entry(keys, i);
|
|
314
|
+
|
|
315
|
+
if (rb_hash_lookup(processed_files, filename) != Qnil) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
rb_hash_aset(processed_files, filename, Qtrue);
|
|
319
|
+
|
|
320
|
+
VALUE const_names = get_const_refs_for_file(filename);
|
|
321
|
+
if (NIL_P(const_names) || !RB_TYPE_P(const_names, T_ARRAY)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
long num_refs = RARRAY_LEN(const_names);
|
|
326
|
+
for (long j = 0; j < num_refs; j++) {
|
|
327
|
+
VALUE const_name = rb_ary_entry(const_names, j);
|
|
328
|
+
|
|
329
|
+
if (rb_hash_lookup(seen_consts, const_name) != Qnil) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
rb_hash_aset(seen_consts, const_name, Qtrue);
|
|
333
|
+
|
|
334
|
+
VALUE resolved_file = fast_cov_resolve_const_to_file(const_name);
|
|
335
|
+
if (NIL_P(resolved_file)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (record_impacted_file(data, resolved_file)) {
|
|
340
|
+
found_new_file = 1;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!found_new_file) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- Utils module methods (FastCov::Utils) ------------------------------
|
|
352
|
+
|
|
353
|
+
// Utils.path_within?(path, directory) -> true/false
|
|
354
|
+
// Check if path is within directory, correctly handling:
|
|
355
|
+
// - Trailing slashes on directory
|
|
356
|
+
// - Sibling directories with longer names (e.g., /a/b/c vs /a/b/cd)
|
|
357
|
+
static VALUE utils_path_within(VALUE self, VALUE path, VALUE directory) {
|
|
358
|
+
Check_Type(path, T_STRING);
|
|
359
|
+
Check_Type(directory, T_STRING);
|
|
360
|
+
|
|
361
|
+
// Freeze strings to prevent GC compaction from moving them
|
|
362
|
+
rb_str_freeze(path);
|
|
363
|
+
rb_str_freeze(directory);
|
|
364
|
+
|
|
365
|
+
bool result = fast_cov_is_within_root(
|
|
366
|
+
RSTRING_PTR(path), RSTRING_LEN(path),
|
|
367
|
+
RSTRING_PTR(directory), RSTRING_LEN(directory));
|
|
368
|
+
|
|
369
|
+
return result ? Qtrue : Qfalse;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Utils.relativize_paths(set, root) -> set
|
|
373
|
+
// Mutates set in place: converts absolute paths to relative paths from root.
|
|
374
|
+
// Paths not within root are left unchanged.
|
|
375
|
+
static VALUE utils_relativize_paths(VALUE self, VALUE set, VALUE root) {
|
|
376
|
+
Check_Type(root, T_STRING);
|
|
377
|
+
|
|
378
|
+
// Freeze root to prevent GC from moving it during compaction
|
|
379
|
+
rb_str_freeze(root);
|
|
380
|
+
|
|
381
|
+
const char *root_ptr = RSTRING_PTR(root);
|
|
382
|
+
long root_len = RSTRING_LEN(root);
|
|
383
|
+
|
|
384
|
+
// Normalize: strip trailing slash for offset calculation
|
|
385
|
+
long effective_root_len = root_len;
|
|
386
|
+
if (effective_root_len > 0 && root_ptr[effective_root_len - 1] == '/') {
|
|
387
|
+
effective_root_len--;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Collect paths to transform (can't modify set while iterating)
|
|
391
|
+
VALUE paths = rb_funcall(set, rb_intern("to_a"), 0);
|
|
392
|
+
long num_paths = RARRAY_LEN(paths);
|
|
393
|
+
|
|
394
|
+
for (long i = 0; i < num_paths; i++) {
|
|
395
|
+
VALUE abs_path = rb_ary_entry(paths, i);
|
|
396
|
+
if (!RB_TYPE_P(abs_path, T_STRING)) continue;
|
|
397
|
+
|
|
398
|
+
// Freeze to prevent GC moving it
|
|
399
|
+
rb_str_freeze(abs_path);
|
|
400
|
+
|
|
401
|
+
const char *path_ptr = RSTRING_PTR(abs_path);
|
|
402
|
+
long path_len = RSTRING_LEN(abs_path);
|
|
403
|
+
|
|
404
|
+
// Use proper within_root check
|
|
405
|
+
if (!fast_cov_is_within_root(path_ptr, path_len, root_ptr, root_len)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Calculate offset (skip root + separator)
|
|
410
|
+
long offset = effective_root_len;
|
|
411
|
+
if (offset < path_len && path_ptr[offset] == '/') offset++;
|
|
412
|
+
|
|
413
|
+
// Create relative path
|
|
414
|
+
VALUE rel_path = rb_str_substr(abs_path, offset, path_len - offset);
|
|
415
|
+
|
|
416
|
+
// Delete old path, add new path
|
|
417
|
+
rb_funcall(set, rb_intern("delete"), 1, abs_path);
|
|
418
|
+
rb_funcall(set, rb_intern("add"), 1, rel_path);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
RB_GC_GUARD(paths);
|
|
422
|
+
return set;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---- Cache module methods (FastCov::Cache) ------------------------------
|
|
426
|
+
|
|
427
|
+
static VALUE cache_get_data(VALUE self) { return fast_cov_cache_hash; }
|
|
428
|
+
|
|
429
|
+
static VALUE cache_set_data(VALUE self, VALUE new_cache) {
|
|
430
|
+
if (!RB_TYPE_P(new_cache, T_HASH)) {
|
|
431
|
+
rb_raise(rb_eTypeError, "cache data must be a Hash");
|
|
432
|
+
}
|
|
433
|
+
rb_funcall(fast_cov_cache_hash, id_clear, 0);
|
|
434
|
+
rb_funcall(fast_cov_cache_hash, id_merge_bang, 1, new_cache);
|
|
435
|
+
return fast_cov_cache_hash;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
static VALUE cache_clear(VALUE self) {
|
|
439
|
+
rb_funcall(fast_cov_cache_hash, id_clear, 0);
|
|
440
|
+
rb_hash_aset(fast_cov_cache_hash, ID2SYM(rb_intern("const_refs")),
|
|
441
|
+
rb_hash_new());
|
|
442
|
+
rb_hash_aset(fast_cov_cache_hash, ID2SYM(rb_intern("const_locations")),
|
|
443
|
+
rb_hash_new());
|
|
444
|
+
return Qnil;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---- Ruby instance methods ----------------------------------------------
|
|
448
|
+
|
|
449
|
+
static VALUE fast_cov_initialize(int argc, VALUE *argv, VALUE self) {
|
|
450
|
+
VALUE opt;
|
|
451
|
+
rb_scan_args(argc, argv, "01", &opt);
|
|
452
|
+
if (NIL_P(opt)) opt = rb_hash_new();
|
|
453
|
+
|
|
454
|
+
// root: defaults to Dir.pwd
|
|
455
|
+
VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
|
|
456
|
+
if (!RTEST(rb_root)) {
|
|
457
|
+
rb_root = rb_funcall(rb_cDir, rb_intern("pwd"), 0);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ignored_path: optional, nil if not provided
|
|
461
|
+
VALUE rb_ignored_path =
|
|
462
|
+
rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));
|
|
463
|
+
|
|
464
|
+
// threads: true (multi) or false (single), defaults to true
|
|
465
|
+
VALUE rb_threads = rb_hash_lookup(opt, ID2SYM(rb_intern("threads")));
|
|
466
|
+
bool threads = (rb_threads != Qfalse);
|
|
467
|
+
|
|
468
|
+
// constant_references: defaults to true
|
|
469
|
+
VALUE rb_const_refs =
|
|
470
|
+
rb_hash_lookup(opt, ID2SYM(rb_intern("constant_references")));
|
|
471
|
+
bool constant_references = (rb_const_refs != Qfalse);
|
|
472
|
+
|
|
473
|
+
// allocations: defaults to true
|
|
474
|
+
VALUE rb_allocations =
|
|
475
|
+
rb_hash_lookup(opt, ID2SYM(rb_intern("allocations")));
|
|
476
|
+
bool allocations = (rb_allocations != Qfalse);
|
|
477
|
+
|
|
478
|
+
struct fast_cov_data *data;
|
|
479
|
+
TypedData_Get_Struct(self, struct fast_cov_data, &fast_cov_data_type, data);
|
|
480
|
+
|
|
481
|
+
data->threads = threads;
|
|
482
|
+
data->constant_references = constant_references;
|
|
483
|
+
data->allocations = allocations;
|
|
484
|
+
data->root_len = RSTRING_LEN(rb_root);
|
|
485
|
+
data->root =
|
|
486
|
+
fast_cov_ruby_strndup(RSTRING_PTR(rb_root), data->root_len);
|
|
487
|
+
|
|
488
|
+
if (RTEST(rb_ignored_path)) {
|
|
489
|
+
data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
|
|
490
|
+
data->ignored_path = fast_cov_ruby_strndup(RSTRING_PTR(rb_ignored_path),
|
|
491
|
+
data->ignored_path_len);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (allocations) {
|
|
495
|
+
data->object_allocation_tracepoint = rb_tracepoint_new(
|
|
496
|
+
Qnil, RUBY_INTERNAL_EVENT_NEWOBJ, on_newobj_event, (void *)data);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return Qnil;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
static VALUE fast_cov_start(VALUE self) {
|
|
503
|
+
struct fast_cov_data *data;
|
|
504
|
+
TypedData_Get_Struct(self, struct fast_cov_data, &fast_cov_data_type, data);
|
|
505
|
+
|
|
506
|
+
if (data->root_len == 0) {
|
|
507
|
+
rb_raise(rb_eRuntimeError, "root is required");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!data->threads) {
|
|
511
|
+
VALUE thval = rb_thread_current();
|
|
512
|
+
rb_thread_add_event_hook(thval, on_line_event, RUBY_EVENT_LINE, self);
|
|
513
|
+
data->th_covered = thval;
|
|
514
|
+
} else {
|
|
515
|
+
rb_add_event_hook(on_line_event, RUBY_EVENT_LINE, self);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (data->object_allocation_tracepoint != Qnil) {
|
|
519
|
+
rb_tracepoint_enable(data->object_allocation_tracepoint);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Block form: start { ... } runs the block then returns stop result
|
|
523
|
+
if (rb_block_given_p()) {
|
|
524
|
+
rb_yield(Qnil);
|
|
525
|
+
return fast_cov_stop(self);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return self;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
static VALUE fast_cov_stop(VALUE self) {
|
|
532
|
+
struct fast_cov_data *data;
|
|
533
|
+
TypedData_Get_Struct(self, struct fast_cov_data, &fast_cov_data_type, data);
|
|
534
|
+
|
|
535
|
+
if (!data->threads) {
|
|
536
|
+
VALUE thval = rb_thread_current();
|
|
537
|
+
if (thval != data->th_covered) {
|
|
538
|
+
rb_raise(rb_eRuntimeError, "Coverage was not started by this thread");
|
|
539
|
+
}
|
|
540
|
+
rb_thread_remove_event_hook(data->th_covered, on_line_event);
|
|
541
|
+
data->th_covered = Qnil;
|
|
542
|
+
} else {
|
|
543
|
+
rb_remove_event_hook(on_line_event);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (data->object_allocation_tracepoint != Qnil) {
|
|
547
|
+
rb_tracepoint_disable(data->object_allocation_tracepoint);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (data->allocations) {
|
|
551
|
+
st_foreach(data->klasses_table, each_instantiated_klass, (st_data_t)data);
|
|
552
|
+
st_clear(data->klasses_table);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (data->constant_references) {
|
|
556
|
+
resolve_constant_references(data);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
VALUE res = data->impacted_files;
|
|
560
|
+
|
|
561
|
+
data->impacted_files = rb_hash_new();
|
|
562
|
+
data->last_filename_ptr = 0;
|
|
563
|
+
|
|
564
|
+
return res;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---- Init ---------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
void Init_fast_cov(void) {
|
|
570
|
+
id_extract = rb_intern("extract");
|
|
571
|
+
id_keys = rb_intern("keys");
|
|
572
|
+
id_file = rb_intern("file");
|
|
573
|
+
id_hexdigest = rb_intern("hexdigest");
|
|
574
|
+
id_clear = rb_intern("clear");
|
|
575
|
+
id_merge_bang = rb_intern("merge!");
|
|
576
|
+
|
|
577
|
+
rb_require("digest/md5");
|
|
578
|
+
rb_require("fast_cov/constant_extractor");
|
|
579
|
+
VALUE mDigest = rb_const_get(rb_cObject, rb_intern("Digest"));
|
|
580
|
+
cDigest = rb_const_get(mDigest, rb_intern("MD5"));
|
|
581
|
+
rb_gc_register_address(&cDigest);
|
|
582
|
+
|
|
583
|
+
// Initialize process-level cache
|
|
584
|
+
fast_cov_cache_hash = rb_hash_new();
|
|
585
|
+
rb_gc_register_address(&fast_cov_cache_hash);
|
|
586
|
+
rb_hash_aset(fast_cov_cache_hash, ID2SYM(rb_intern("const_refs")),
|
|
587
|
+
rb_hash_new());
|
|
588
|
+
rb_hash_aset(fast_cov_cache_hash, ID2SYM(rb_intern("const_locations")),
|
|
589
|
+
rb_hash_new());
|
|
590
|
+
|
|
591
|
+
VALUE mFastCov = rb_define_module("FastCov");
|
|
592
|
+
|
|
593
|
+
// FastCov::ConstantExtractor must be loaded before the C extension
|
|
594
|
+
cConstantExtractor =
|
|
595
|
+
rb_const_get(mFastCov, rb_intern("ConstantExtractor"));
|
|
596
|
+
rb_gc_register_address(&cConstantExtractor);
|
|
597
|
+
|
|
598
|
+
VALUE cCoverage = rb_define_class_under(mFastCov, "Coverage", rb_cObject);
|
|
599
|
+
|
|
600
|
+
rb_define_alloc_func(cCoverage, fast_cov_allocate);
|
|
601
|
+
rb_define_method(cCoverage, "initialize", fast_cov_initialize, -1);
|
|
602
|
+
rb_define_method(cCoverage, "start", fast_cov_start, 0);
|
|
603
|
+
rb_define_method(cCoverage, "stop", fast_cov_stop, 0);
|
|
604
|
+
|
|
605
|
+
// FastCov::Cache module (C-defined methods)
|
|
606
|
+
VALUE mCache = rb_define_module_under(mFastCov, "Cache");
|
|
607
|
+
rb_define_module_function(mCache, "data", cache_get_data, 0);
|
|
608
|
+
rb_define_module_function(mCache, "data=", cache_set_data, 1);
|
|
609
|
+
rb_define_module_function(mCache, "clear", cache_clear, 0);
|
|
610
|
+
|
|
611
|
+
// FastCov::Utils module (C-defined methods)
|
|
612
|
+
VALUE mUtils = rb_define_module_under(mFastCov, "Utils");
|
|
613
|
+
rb_define_module_function(mUtils, "path_within?", utils_path_within, 2);
|
|
614
|
+
rb_define_module_function(mUtils, "relativize_paths", utils_relativize_paths, 2);
|
|
615
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#ifndef FAST_COV_H
|
|
2
|
+
#define FAST_COV_H
|
|
3
|
+
|
|
4
|
+
#include <ruby.h>
|
|
5
|
+
#include <stdbool.h>
|
|
6
|
+
|
|
7
|
+
/* ---- Cache -------------------------------------------------------------- */
|
|
8
|
+
|
|
9
|
+
extern VALUE fast_cov_cache_hash;
|
|
10
|
+
|
|
11
|
+
/* ---- Path filtering ----------------------------------------------------- */
|
|
12
|
+
|
|
13
|
+
bool fast_cov_is_within_root(const char *path, long path_len,
|
|
14
|
+
const char *root, long root_len);
|
|
15
|
+
|
|
16
|
+
bool fast_cov_is_path_included(const char *path, const char *root_path,
|
|
17
|
+
long root_path_len, const char *ignored_path,
|
|
18
|
+
long ignored_path_len);
|
|
19
|
+
|
|
20
|
+
/* ---- Utility functions -------------------------------------------------- */
|
|
21
|
+
|
|
22
|
+
char *fast_cov_ruby_strndup(const char *str, size_t size);
|
|
23
|
+
|
|
24
|
+
VALUE fast_cov_rescue_nil(VALUE (*fn)(VALUE), VALUE arg);
|
|
25
|
+
|
|
26
|
+
VALUE fast_cov_get_const_source_location(VALUE const_name_str);
|
|
27
|
+
|
|
28
|
+
VALUE fast_cov_safely_get_const_source_location(VALUE const_name_str);
|
|
29
|
+
|
|
30
|
+
VALUE fast_cov_resolve_const_to_file(VALUE const_name_str);
|
|
31
|
+
|
|
32
|
+
#endif /* FAST_COV_H */
|