datadog-ci 1.36.1 → 1.37.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/CHANGELOG.md +4 -1
- data/ext/datadog_ci_native/ci.c +4 -0
- data/ext/datadog_ci_native/datadog_common.c +17 -6
- data/ext/datadog_ci_native/datadog_common.h +4 -3
- data/ext/datadog_ci_native/datadog_cov.c +182 -55
- data/ext/datadog_ci_native/file_serialization.c +279 -0
- data/ext/datadog_ci_native/file_serialization.h +3 -0
- data/lib/datadog/ci/contrib/instrumentation.rb +14 -0
- data/lib/datadog/ci/contrib/parallel_tests/cli.rb +5 -0
- data/lib/datadog/ci/file_serialization.rb +12 -0
- data/lib/datadog/ci/source_code/path_filter.rb +12 -4
- data/lib/datadog/ci/span.rb +9 -5
- data/lib/datadog/ci/test_impact_analysis/coverage/event.rb +1 -6
- data/lib/datadog/ci/test_impact_analysis/coverage/files.rb +29 -0
- data/lib/datadog/ci/test_tracing/component.rb +5 -0
- data/lib/datadog/ci/version.rb +2 -2
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a4aa364e13169f17cd0c37cc4dff152b1f8861895569d7d5bdf0a9398d2415f
|
|
4
|
+
data.tar.gz: 900cc26e588995ffe4dde4c32a42aee348593adf521a498d82d138233595861b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9a3e6033dd78d3570da5184f1e220abdf7c3aeb7a9ed76cd1ff25440d906ea0f2a20670930270677c4e17826e6a7283ad411a71cdd446c01b16340dc5c86cce
|
|
7
|
+
data.tar.gz: 5e5eb24079c51dc28c78a22c07e185e6501917ef9dcb448b812cb76dc5243efe84fbe132042b1049bfc626c93ae057e8db954cad48c9f8a40e93dafdddfb3daf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.37.0] - 2026-08-19
|
|
4
|
+
|
|
3
5
|
## [1.36.1] - 2026-08-11
|
|
4
6
|
|
|
5
7
|
### Fixed
|
|
@@ -682,7 +684,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
|
|
|
682
684
|
|
|
683
685
|
- Ruby versions < 2.7 no longer supported ([#8][])
|
|
684
686
|
|
|
685
|
-
[Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.
|
|
687
|
+
[Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.37.0...main
|
|
688
|
+
[1.37.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.36.1...v1.37.0
|
|
686
689
|
[1.36.1]: https://github.com/DataDog/datadog-ci-rb/compare/v1.36.0...v1.36.1
|
|
687
690
|
[1.36.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.35.0...v1.36.0
|
|
688
691
|
[1.35.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...v1.35.0
|
data/ext/datadog_ci_native/ci.c
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#include "datadog_cov.h"
|
|
2
2
|
#include "datadog_method_inspect.h"
|
|
3
|
+
#include "file_serialization.h"
|
|
3
4
|
#include "iseq_collector.h"
|
|
4
5
|
|
|
5
6
|
void Init_datadog_ci_native(void) {
|
|
6
7
|
// Coverage::DDCov
|
|
7
8
|
Init_datadog_cov();
|
|
8
9
|
|
|
10
|
+
// FileSerialization
|
|
11
|
+
Init_file_serialization();
|
|
12
|
+
|
|
9
13
|
// SourceCode
|
|
10
14
|
Init_datadog_method_inspect();
|
|
11
15
|
Init_dd_ci_iseq_collector();
|
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
#include <ruby.h>
|
|
3
3
|
#include <string.h>
|
|
4
4
|
|
|
5
|
-
bool
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (strncmp(root_path, path, root_path_len) != 0) {
|
|
5
|
+
static bool has_directory_prefix(const char *path, long path_len,
|
|
6
|
+
const char *prefix, long prefix_len) {
|
|
7
|
+
if (prefix_len > path_len || memcmp(prefix, path, prefix_len) != 0) {
|
|
9
8
|
return false;
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
return prefix_len == path_len || prefix[prefix_len - 1] == '/' ||
|
|
12
|
+
path[prefix_len] == '/';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
bool dd_ci_is_path_included(const char *path, long path_len,
|
|
16
|
+
const char *root_path, long root_path_len,
|
|
17
|
+
const char *ignored_path, long ignored_path_len) {
|
|
18
|
+
if (!has_directory_prefix(path, path_len, root_path, root_path_len)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (ignored_path_len > 0 && has_directory_prefix(
|
|
22
|
+
path, path_len, ignored_path,
|
|
23
|
+
ignored_path_len)) {
|
|
13
24
|
return false;
|
|
14
25
|
}
|
|
15
26
|
return true;
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* Returns true if the path should be included, false otherwise.
|
|
12
12
|
*
|
|
13
13
|
* @param path The file path to check
|
|
14
|
+
* @param path_len Length of path
|
|
14
15
|
* @param root_path The root path prefix (required)
|
|
15
16
|
* @param root_path_len Length of root_path
|
|
16
17
|
* @param ignored_path Path prefix to exclude (can be NULL)
|
|
17
18
|
* @param ignored_path_len Length of ignored_path (0 if not set)
|
|
18
19
|
*/
|
|
19
|
-
bool dd_ci_is_path_included(const char *path,
|
|
20
|
-
|
|
21
|
-
long ignored_path_len);
|
|
20
|
+
bool dd_ci_is_path_included(const char *path, long path_len,
|
|
21
|
+
const char *root_path, long root_path_len,
|
|
22
|
+
const char *ignored_path, long ignored_path_len);
|
|
22
23
|
|
|
23
24
|
/* ---- Utility functions -------------------------------------------------- */
|
|
24
25
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <ruby/st.h>
|
|
4
4
|
|
|
5
5
|
#include <stdbool.h>
|
|
6
|
+
#include <string.h>
|
|
6
7
|
|
|
7
8
|
#include "datadog_common.h"
|
|
8
9
|
|
|
@@ -11,12 +12,26 @@
|
|
|
11
12
|
// running only the tests that are affected by the changes.
|
|
12
13
|
|
|
13
14
|
#define PROFILE_FRAMES_BUFFER_SIZE 1
|
|
15
|
+
#define SEEN_FILENAME_CACHE_SIZE 1024
|
|
16
|
+
#define SEEN_ALLOCATED_CLASS_CACHE_SIZE 4096
|
|
17
|
+
#define KLASS_FILES_CACHE_SIZE 100000
|
|
18
|
+
|
|
19
|
+
#if SEEN_FILENAME_CACHE_SIZE == 0 || \
|
|
20
|
+
(SEEN_FILENAME_CACHE_SIZE & (SEEN_FILENAME_CACHE_SIZE - 1)) != 0
|
|
21
|
+
#error "SEEN_FILENAME_CACHE_SIZE must be a power of two"
|
|
22
|
+
#endif
|
|
23
|
+
|
|
24
|
+
#if SEEN_ALLOCATED_CLASS_CACHE_SIZE == 0 || \
|
|
25
|
+
(SEEN_ALLOCATED_CLASS_CACHE_SIZE & \
|
|
26
|
+
(SEEN_ALLOCATED_CLASS_CACHE_SIZE - 1)) != 0
|
|
27
|
+
#error "SEEN_ALLOCATED_CLASS_CACHE_SIZE must be a power of two"
|
|
28
|
+
#endif
|
|
14
29
|
|
|
15
30
|
// threading modes
|
|
16
31
|
enum threading_mode { single, multi };
|
|
17
32
|
|
|
18
33
|
// functions declarations
|
|
19
|
-
static void on_newobj_event(VALUE
|
|
34
|
+
static void on_newobj_event(VALUE self, const rb_trace_arg_t *tracearg);
|
|
20
35
|
|
|
21
36
|
static int mark_key_for_gc_i(st_data_t key, st_data_t _value, st_data_t _data) {
|
|
22
37
|
VALUE klass = (VALUE)key;
|
|
@@ -25,6 +40,15 @@ static int mark_key_for_gc_i(st_data_t key, st_data_t _value, st_data_t _data) {
|
|
|
25
40
|
return ST_CONTINUE;
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
static int mark_klass_files_for_gc_i(st_data_t key, st_data_t value,
|
|
44
|
+
st_data_t _data) {
|
|
45
|
+
// Both the class key and its cached filenames must remain at stable addresses
|
|
46
|
+
// because they are stored outside Ruby's object containers.
|
|
47
|
+
rb_gc_mark((VALUE)key);
|
|
48
|
+
rb_gc_mark((VALUE)value);
|
|
49
|
+
return ST_CONTINUE;
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
// Data structure
|
|
29
53
|
struct dd_cov_data {
|
|
30
54
|
// Ruby hash with filenames impacted by the test.
|
|
@@ -40,9 +64,12 @@ struct dd_cov_data {
|
|
|
40
64
|
char *ignored_path;
|
|
41
65
|
long ignored_path_len;
|
|
42
66
|
|
|
43
|
-
// Line tracepoint optimisation:
|
|
44
|
-
//
|
|
45
|
-
|
|
67
|
+
// Line tracepoint optimisation: make consecutive events from the same file a
|
|
68
|
+
// single comparison, then use a direct-mapped cache for later revisits. Keep
|
|
69
|
+
// the Ruby strings alive so their pointers cannot be reused for other paths.
|
|
70
|
+
// A collision only repeats path processing; it can never suppress coverage.
|
|
71
|
+
VALUE last_filename;
|
|
72
|
+
VALUE seen_filenames[SEEN_FILENAME_CACHE_SIZE];
|
|
46
73
|
|
|
47
74
|
// Line tracepoint can work in two modes: single threaded and multi threaded
|
|
48
75
|
//
|
|
@@ -61,22 +88,38 @@ struct dd_cov_data {
|
|
|
61
88
|
// contain any methods that could be covered by line tracepoint.
|
|
62
89
|
//
|
|
63
90
|
// Allocation tracing works only in multi threaded mode.
|
|
64
|
-
|
|
91
|
+
bool allocation_tracing_enabled;
|
|
92
|
+
bool allocation_hook_active;
|
|
65
93
|
st_table *klasses_table; // { (VALUE) -> int } hashmap with class names that
|
|
66
94
|
// were covered by allocation during the test run
|
|
95
|
+
VALUE last_allocated_klass;
|
|
96
|
+
VALUE seen_allocated_klasses[SEEN_ALLOCATED_CLASS_CACHE_SIZE];
|
|
97
|
+
st_table *klass_files_cache; // { (VALUE) -> Array<String> } resolved files
|
|
98
|
+
size_t klass_files_cache_size;
|
|
67
99
|
};
|
|
68
100
|
|
|
69
101
|
static void dd_cov_mark(void *ptr) {
|
|
70
102
|
struct dd_cov_data *dd_cov_data = ptr;
|
|
71
103
|
rb_gc_mark_movable(dd_cov_data->impacted_files);
|
|
72
104
|
rb_gc_mark_movable(dd_cov_data->th_covered);
|
|
73
|
-
|
|
105
|
+
|
|
106
|
+
if (dd_cov_data->last_filename != Qnil) {
|
|
107
|
+
rb_gc_mark(dd_cov_data->last_filename);
|
|
108
|
+
}
|
|
109
|
+
for (size_t i = 0; i < SEEN_FILENAME_CACHE_SIZE; i++) {
|
|
110
|
+
if (dd_cov_data->seen_filenames[i] != Qnil) {
|
|
111
|
+
rb_gc_mark(dd_cov_data->seen_filenames[i]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
74
114
|
|
|
75
115
|
// if GC starts withing dd_cov_allocate() call, klasses_table might not be
|
|
76
116
|
// initialized yet
|
|
77
117
|
if (dd_cov_data->klasses_table != NULL) {
|
|
78
118
|
st_foreach(dd_cov_data->klasses_table, mark_key_for_gc_i, 0);
|
|
79
119
|
}
|
|
120
|
+
if (dd_cov_data->klass_files_cache != NULL) {
|
|
121
|
+
st_foreach(dd_cov_data->klass_files_cache, mark_klass_files_for_gc_i, 0);
|
|
122
|
+
}
|
|
80
123
|
}
|
|
81
124
|
|
|
82
125
|
static void dd_cov_free(void *ptr) {
|
|
@@ -84,6 +127,7 @@ static void dd_cov_free(void *ptr) {
|
|
|
84
127
|
xfree(dd_cov_data->root);
|
|
85
128
|
xfree(dd_cov_data->ignored_path);
|
|
86
129
|
st_free_table(dd_cov_data->klasses_table);
|
|
130
|
+
st_free_table(dd_cov_data->klass_files_cache);
|
|
87
131
|
xfree(dd_cov_data);
|
|
88
132
|
}
|
|
89
133
|
|
|
@@ -91,8 +135,6 @@ static void dd_cov_compact(void *ptr) {
|
|
|
91
135
|
struct dd_cov_data *dd_cov_data = ptr;
|
|
92
136
|
dd_cov_data->impacted_files = rb_gc_location(dd_cov_data->impacted_files);
|
|
93
137
|
dd_cov_data->th_covered = rb_gc_location(dd_cov_data->th_covered);
|
|
94
|
-
dd_cov_data->object_allocation_tracepoint =
|
|
95
|
-
rb_gc_location(dd_cov_data->object_allocation_tracepoint);
|
|
96
138
|
// keys for dd_cov_data->klasses_table are not moved by GC, so we don't need
|
|
97
139
|
// to update them
|
|
98
140
|
}
|
|
@@ -115,12 +157,21 @@ static VALUE dd_cov_allocate(VALUE klass) {
|
|
|
115
157
|
dd_cov_data->root_len = 0;
|
|
116
158
|
dd_cov_data->ignored_path = NULL;
|
|
117
159
|
dd_cov_data->ignored_path_len = 0;
|
|
118
|
-
dd_cov_data->
|
|
160
|
+
dd_cov_data->last_filename = Qnil;
|
|
161
|
+
for (size_t i = 0; i < SEEN_FILENAME_CACHE_SIZE; i++) {
|
|
162
|
+
dd_cov_data->seen_filenames[i] = Qnil;
|
|
163
|
+
}
|
|
119
164
|
dd_cov_data->threading_mode = multi;
|
|
120
165
|
|
|
121
|
-
dd_cov_data->
|
|
166
|
+
dd_cov_data->allocation_tracing_enabled = false;
|
|
167
|
+
dd_cov_data->allocation_hook_active = false;
|
|
168
|
+
dd_cov_data->last_allocated_klass = Qnil;
|
|
169
|
+
memset(dd_cov_data->seen_allocated_klasses, 0,
|
|
170
|
+
sizeof(dd_cov_data->seen_allocated_klasses));
|
|
122
171
|
// numtable type is needed to store VALUE as a key
|
|
123
172
|
dd_cov_data->klasses_table = st_init_numtable();
|
|
173
|
+
dd_cov_data->klass_files_cache = st_init_numtable();
|
|
174
|
+
dd_cov_data->klass_files_cache_size = 0;
|
|
124
175
|
|
|
125
176
|
return dd_cov;
|
|
126
177
|
}
|
|
@@ -131,8 +182,9 @@ static VALUE dd_cov_allocate(VALUE klass) {
|
|
|
131
182
|
// not in the ignored folder) and adds it to the impacted_files hash.
|
|
132
183
|
static bool record_impacted_file(struct dd_cov_data *dd_cov_data,
|
|
133
184
|
VALUE filename) {
|
|
134
|
-
if (!dd_ci_is_path_included(RSTRING_PTR(filename),
|
|
135
|
-
dd_cov_data->
|
|
185
|
+
if (!dd_ci_is_path_included(RSTRING_PTR(filename), RSTRING_LEN(filename),
|
|
186
|
+
dd_cov_data->root, dd_cov_data->root_len,
|
|
187
|
+
dd_cov_data->ignored_path,
|
|
136
188
|
dd_cov_data->ignored_path_len)) {
|
|
137
189
|
return false;
|
|
138
190
|
}
|
|
@@ -145,18 +197,29 @@ static bool record_impacted_file(struct dd_cov_data *dd_cov_data,
|
|
|
145
197
|
// rb_profile_frames.
|
|
146
198
|
static void on_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id,
|
|
147
199
|
VALUE klass) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
200
|
+
// The hook is registered only with DDCov instances, so the full typed-data
|
|
201
|
+
// type check on every Ruby line is unnecessary.
|
|
202
|
+
struct dd_cov_data *dd_cov_data = RTYPEDDATA_DATA(data);
|
|
151
203
|
|
|
152
204
|
const char *c_filename = rb_sourcefile();
|
|
205
|
+
if (c_filename == NULL) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
153
208
|
|
|
154
|
-
// skip if we cover the same file again
|
|
155
209
|
uintptr_t current_filename_ptr = (uintptr_t)c_filename;
|
|
156
|
-
if (dd_cov_data->
|
|
210
|
+
if (dd_cov_data->last_filename != Qnil &&
|
|
211
|
+
RSTRING_PTR(dd_cov_data->last_filename) == c_filename) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
size_t cache_index =
|
|
216
|
+
((current_filename_ptr >> 4) ^ (current_filename_ptr >> 12)) &
|
|
217
|
+
(SEEN_FILENAME_CACHE_SIZE - 1);
|
|
218
|
+
VALUE cached_filename = dd_cov_data->seen_filenames[cache_index];
|
|
219
|
+
if (cached_filename != Qnil && RSTRING_PTR(cached_filename) == c_filename) {
|
|
220
|
+
dd_cov_data->last_filename = cached_filename;
|
|
157
221
|
return;
|
|
158
222
|
}
|
|
159
|
-
dd_cov_data->last_filename_ptr = current_filename_ptr;
|
|
160
223
|
|
|
161
224
|
VALUE top_frame;
|
|
162
225
|
int captured_frames =
|
|
@@ -172,6 +235,8 @@ static void on_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id,
|
|
|
172
235
|
return;
|
|
173
236
|
}
|
|
174
237
|
|
|
238
|
+
dd_cov_data->last_filename = filename;
|
|
239
|
+
dd_cov_data->seen_filenames[cache_index] = filename;
|
|
175
240
|
record_impacted_file(dd_cov_data, filename);
|
|
176
241
|
}
|
|
177
242
|
|
|
@@ -185,21 +250,6 @@ static VALUE safely_get_mod_ancestors(VALUE klass) {
|
|
|
185
250
|
return dd_ci_rescue_nil(rb_mod_ancestors, klass);
|
|
186
251
|
}
|
|
187
252
|
|
|
188
|
-
static bool record_impacted_klass(struct dd_cov_data *dd_cov_data,
|
|
189
|
-
VALUE klass) {
|
|
190
|
-
VALUE klass_name = safely_get_class_name(klass);
|
|
191
|
-
if (klass_name == Qnil) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
VALUE filename = dd_ci_resolve_const_to_file(klass_name);
|
|
196
|
-
if (filename == Qnil) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return record_impacted_file(dd_cov_data, filename);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
253
|
// This function is called for each class that was instantiated during the test
|
|
204
254
|
// run.
|
|
205
255
|
static int each_instantiated_klass(st_data_t key, st_data_t _value,
|
|
@@ -207,6 +257,18 @@ static int each_instantiated_klass(st_data_t key, st_data_t _value,
|
|
|
207
257
|
VALUE klass = (VALUE)key;
|
|
208
258
|
struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
|
|
209
259
|
|
|
260
|
+
st_data_t cached_files;
|
|
261
|
+
if (st_lookup(dd_cov_data->klass_files_cache, key, &cached_files)) {
|
|
262
|
+
VALUE files = (VALUE)cached_files;
|
|
263
|
+
long files_len = RARRAY_LEN(files);
|
|
264
|
+
for (long i = 0; i < files_len; i++) {
|
|
265
|
+
rb_hash_aset(dd_cov_data->impacted_files, rb_ary_entry(files, i), Qtrue);
|
|
266
|
+
}
|
|
267
|
+
return ST_CONTINUE;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
VALUE files = rb_ary_new();
|
|
271
|
+
|
|
210
272
|
// rb_mod_ancestors returns an array containing the "klass" itself
|
|
211
273
|
// and all the parent classes and/or included/prepended modules
|
|
212
274
|
VALUE ancestors = safely_get_mod_ancestors(klass);
|
|
@@ -221,17 +283,36 @@ static int each_instantiated_klass(st_data_t key, st_data_t _value,
|
|
|
221
283
|
continue;
|
|
222
284
|
}
|
|
223
285
|
|
|
224
|
-
|
|
286
|
+
VALUE klass_name = safely_get_class_name(mod);
|
|
287
|
+
if (klass_name == Qnil) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
VALUE filename = dd_ci_resolve_const_to_file(klass_name);
|
|
292
|
+
if (filename == Qnil || !record_impacted_file(dd_cov_data, filename)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
rb_ary_push(files, filename);
|
|
225
297
|
}
|
|
226
298
|
|
|
299
|
+
rb_obj_freeze(files);
|
|
300
|
+
// Bound cross-test retention without adding eviction work to cache hits.
|
|
301
|
+
// Reaching the limit starts a fresh cache generation.
|
|
302
|
+
if (dd_cov_data->klass_files_cache_size >= KLASS_FILES_CACHE_SIZE) {
|
|
303
|
+
st_clear(dd_cov_data->klass_files_cache);
|
|
304
|
+
dd_cov_data->klass_files_cache_size = 0;
|
|
305
|
+
}
|
|
306
|
+
st_insert(dd_cov_data->klass_files_cache, key, (st_data_t)files);
|
|
307
|
+
dd_cov_data->klass_files_cache_size++;
|
|
308
|
+
|
|
227
309
|
return ST_CONTINUE;
|
|
228
310
|
}
|
|
229
311
|
|
|
230
312
|
// Executed on RUBY_INTERNAL_EVENT_NEWOBJ event and captures the source file for
|
|
231
313
|
// the allocated object's class.
|
|
232
|
-
static void on_newobj_event(VALUE
|
|
233
|
-
|
|
234
|
-
VALUE new_object = rb_tracearg_object(tracearg);
|
|
314
|
+
static void on_newobj_event(VALUE self, const rb_trace_arg_t *tracearg) {
|
|
315
|
+
VALUE new_object = rb_tracearg_object((rb_trace_arg_t *)tracearg);
|
|
235
316
|
|
|
236
317
|
// To keep things fast and practical, we only care about objects that extend
|
|
237
318
|
// either Object or Struct.
|
|
@@ -244,20 +325,41 @@ static void on_newobj_event(VALUE tracepoint_data, void *data) {
|
|
|
244
325
|
if (klass == Qnil || klass == 0) {
|
|
245
326
|
return;
|
|
246
327
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (rb_mod_name(klass) == Qnil) {
|
|
328
|
+
|
|
329
|
+
struct dd_cov_data *dd_cov_data = RTYPEDDATA_DATA(self);
|
|
330
|
+
|
|
331
|
+
if (dd_cov_data->last_allocated_klass == klass) {
|
|
252
332
|
return;
|
|
253
333
|
}
|
|
254
334
|
|
|
255
|
-
|
|
335
|
+
uintptr_t klass_ptr = (uintptr_t)klass;
|
|
336
|
+
size_t cache_index =
|
|
337
|
+
((klass_ptr >> 4) ^ (klass_ptr >> 12)) &
|
|
338
|
+
(SEEN_ALLOCATED_CLASS_CACHE_SIZE - 1);
|
|
339
|
+
if (dd_cov_data->seen_allocated_klasses[cache_index] == klass) {
|
|
340
|
+
dd_cov_data->last_allocated_klass = klass;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// A direct-cache collision must not make us repeat name resolution or table
|
|
345
|
+
// insertion for a class that this test already observed.
|
|
346
|
+
if (st_is_member(dd_cov_data->klasses_table, (st_data_t)klass)) {
|
|
347
|
+
dd_cov_data->last_allocated_klass = klass;
|
|
348
|
+
dd_cov_data->seen_allocated_klasses[cache_index] = klass;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// rb_mod_name returns nil for anonymous classes and is safe during NEWOBJ.
|
|
353
|
+
if (rb_mod_name(klass) == Qnil) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
256
356
|
|
|
257
357
|
// We use VALUE directly as a key for the hashmap
|
|
258
358
|
// Ruby itself does it too:
|
|
259
359
|
// https://github.com/ruby/ruby/blob/94b87084a689a3bc732dcaee744508a708223d6c/ext/objspace/object_tracing.c#L113
|
|
260
360
|
st_insert(dd_cov_data->klasses_table, (st_data_t)klass, 1);
|
|
361
|
+
dd_cov_data->last_allocated_klass = klass;
|
|
362
|
+
dd_cov_data->seen_allocated_klasses[cache_index] = klass;
|
|
261
363
|
}
|
|
262
364
|
|
|
263
365
|
// DDCov instance methods available in Ruby
|
|
@@ -265,12 +367,22 @@ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
265
367
|
VALUE opt;
|
|
266
368
|
|
|
267
369
|
rb_scan_args(argc, argv, "10", &opt);
|
|
370
|
+
Check_Type(opt, T_HASH);
|
|
371
|
+
|
|
268
372
|
VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
|
|
269
373
|
if (!RTEST(rb_root)) {
|
|
270
374
|
rb_raise(rb_eArgError, "root is required");
|
|
271
375
|
}
|
|
376
|
+
Check_Type(rb_root, T_STRING);
|
|
377
|
+
const char *root = StringValueCStr(rb_root);
|
|
378
|
+
|
|
272
379
|
VALUE rb_ignored_path =
|
|
273
380
|
rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));
|
|
381
|
+
const char *ignored_path = NULL;
|
|
382
|
+
if (RTEST(rb_ignored_path)) {
|
|
383
|
+
Check_Type(rb_ignored_path, T_STRING);
|
|
384
|
+
ignored_path = StringValueCStr(rb_ignored_path);
|
|
385
|
+
}
|
|
274
386
|
|
|
275
387
|
VALUE rb_threading_mode =
|
|
276
388
|
rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode")));
|
|
@@ -296,18 +408,16 @@ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
296
408
|
|
|
297
409
|
dd_cov_data->threading_mode = threading_mode;
|
|
298
410
|
dd_cov_data->root_len = RSTRING_LEN(rb_root);
|
|
299
|
-
dd_cov_data->root =
|
|
300
|
-
dd_ci_ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);
|
|
411
|
+
dd_cov_data->root = dd_ci_ruby_strndup(root, dd_cov_data->root_len);
|
|
301
412
|
|
|
302
413
|
if (RTEST(rb_ignored_path)) {
|
|
303
414
|
dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
|
|
304
415
|
dd_cov_data->ignored_path = dd_ci_ruby_strndup(
|
|
305
|
-
|
|
416
|
+
ignored_path, dd_cov_data->ignored_path_len);
|
|
306
417
|
}
|
|
307
418
|
|
|
308
419
|
if (rb_allocation_tracing_enabled == Qtrue) {
|
|
309
|
-
dd_cov_data->
|
|
310
|
-
Qnil, RUBY_INTERNAL_EVENT_NEWOBJ, on_newobj_event, (void *)dd_cov_data);
|
|
420
|
+
dd_cov_data->allocation_tracing_enabled = true;
|
|
311
421
|
}
|
|
312
422
|
|
|
313
423
|
return Qnil;
|
|
@@ -332,9 +442,18 @@ static VALUE dd_cov_start(VALUE self) {
|
|
|
332
442
|
rb_add_event_hook(on_line_event, RUBY_EVENT_LINE, self);
|
|
333
443
|
}
|
|
334
444
|
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
445
|
+
// Register the raw hook that TracePoint would wrap and dispatch directly to
|
|
446
|
+
// the allocation callback. NEWOBJ permits no general Ruby API; the callback
|
|
447
|
+
// is limited to the existing event-safe accessors plus rb_mod_name, which is
|
|
448
|
+
// explicitly safe here. Each collector owns its hook, preserving concurrent
|
|
449
|
+
// collector behavior.
|
|
450
|
+
if (dd_cov_data->allocation_tracing_enabled &&
|
|
451
|
+
!dd_cov_data->allocation_hook_active) {
|
|
452
|
+
rb_add_event_hook2((rb_event_hook_func_t)on_newobj_event,
|
|
453
|
+
RUBY_INTERNAL_EVENT_NEWOBJ, self,
|
|
454
|
+
RUBY_EVENT_HOOK_FLAG_SAFE |
|
|
455
|
+
RUBY_EVENT_HOOK_FLAG_RAW_ARG);
|
|
456
|
+
dd_cov_data->allocation_hook_active = true;
|
|
338
457
|
}
|
|
339
458
|
|
|
340
459
|
return self;
|
|
@@ -360,20 +479,28 @@ static VALUE dd_cov_stop(VALUE self) {
|
|
|
360
479
|
rb_remove_event_hook(on_line_event);
|
|
361
480
|
}
|
|
362
481
|
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
482
|
+
// Remove only this collector's hook; other concurrently active collectors
|
|
483
|
+
// continue to receive allocation events.
|
|
484
|
+
if (dd_cov_data->allocation_hook_active) {
|
|
485
|
+
rb_remove_event_hook_with_data((rb_event_hook_func_t)on_newobj_event, self);
|
|
486
|
+
dd_cov_data->allocation_hook_active = false;
|
|
366
487
|
}
|
|
367
488
|
|
|
368
489
|
// process classes covered by allocation tracing
|
|
369
490
|
st_foreach(dd_cov_data->klasses_table, each_instantiated_klass,
|
|
370
491
|
(st_data_t)dd_cov_data);
|
|
492
|
+
dd_cov_data->last_allocated_klass = Qnil;
|
|
493
|
+
memset(dd_cov_data->seen_allocated_klasses, 0,
|
|
494
|
+
sizeof(dd_cov_data->seen_allocated_klasses));
|
|
371
495
|
st_clear(dd_cov_data->klasses_table);
|
|
372
496
|
|
|
373
497
|
VALUE res = dd_cov_data->impacted_files;
|
|
374
498
|
|
|
375
499
|
dd_cov_data->impacted_files = rb_hash_new();
|
|
376
|
-
dd_cov_data->
|
|
500
|
+
dd_cov_data->last_filename = Qnil;
|
|
501
|
+
for (size_t i = 0; i < SEEN_FILENAME_CACHE_SIZE; i++) {
|
|
502
|
+
dd_cov_data->seen_filenames[i] = Qnil;
|
|
503
|
+
}
|
|
377
504
|
|
|
378
505
|
return res;
|
|
379
506
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/encoding.h>
|
|
3
|
+
#include <ruby/st.h>
|
|
4
|
+
|
|
5
|
+
#include <stdbool.h>
|
|
6
|
+
#include <stdint.h>
|
|
7
|
+
#include <string.h>
|
|
8
|
+
|
|
9
|
+
#include "file_serialization.h"
|
|
10
|
+
|
|
11
|
+
// Bulk file serialization is independent from coverage collection. It packs
|
|
12
|
+
// large file lists without building normalized intermediate Ruby collections.
|
|
13
|
+
|
|
14
|
+
struct packed_files_context {
|
|
15
|
+
VALUE root;
|
|
16
|
+
VALUE seen;
|
|
17
|
+
VALUE packed;
|
|
18
|
+
long root_len;
|
|
19
|
+
uint32_t files_count;
|
|
20
|
+
bool direct_absolute;
|
|
21
|
+
bool fast_path_supported;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static bool string_bytes_are_ascii(VALUE string) {
|
|
25
|
+
const unsigned char *ptr = (const unsigned char *)RSTRING_PTR(string);
|
|
26
|
+
long len = RSTRING_LEN(string);
|
|
27
|
+
for (long i = 0; i < len; i++) {
|
|
28
|
+
if ((ptr[i] & 0x80U) != 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static void packed_files_append_u16(VALUE packed, uint16_t value) {
|
|
36
|
+
unsigned char bytes[2] = {(unsigned char)(value >> 8),
|
|
37
|
+
(unsigned char)value};
|
|
38
|
+
rb_str_cat(packed, (const char *)bytes, 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static void packed_files_append_u32(VALUE packed, uint32_t value) {
|
|
42
|
+
unsigned char bytes[4] = {
|
|
43
|
+
(unsigned char)(value >> 24), (unsigned char)(value >> 16),
|
|
44
|
+
(unsigned char)(value >> 8), (unsigned char)value};
|
|
45
|
+
rb_str_cat(packed, (const char *)bytes, 4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static void packed_files_append_string_header(VALUE packed, uint32_t len,
|
|
49
|
+
bool binary) {
|
|
50
|
+
unsigned char byte;
|
|
51
|
+
if (binary) {
|
|
52
|
+
if (len < 256) {
|
|
53
|
+
byte = 0xc4;
|
|
54
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
55
|
+
byte = (unsigned char)len;
|
|
56
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
57
|
+
} else if (len < 65536) {
|
|
58
|
+
byte = 0xc5;
|
|
59
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
60
|
+
packed_files_append_u16(packed, (uint16_t)len);
|
|
61
|
+
} else {
|
|
62
|
+
byte = 0xc6;
|
|
63
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
64
|
+
packed_files_append_u32(packed, len);
|
|
65
|
+
}
|
|
66
|
+
} else if (len < 32) {
|
|
67
|
+
byte = (unsigned char)(0xa0U | len);
|
|
68
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
69
|
+
} else if (len < 256) {
|
|
70
|
+
byte = 0xd9;
|
|
71
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
72
|
+
byte = (unsigned char)len;
|
|
73
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
74
|
+
} else if (len < 65536) {
|
|
75
|
+
byte = 0xda;
|
|
76
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
77
|
+
packed_files_append_u16(packed, (uint16_t)len);
|
|
78
|
+
} else {
|
|
79
|
+
byte = 0xdb;
|
|
80
|
+
rb_str_cat(packed, (const char *)&byte, 1);
|
|
81
|
+
packed_files_append_u32(packed, len);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static bool packed_files_append_entry(struct packed_files_context *context,
|
|
86
|
+
VALUE file, bool additional_file) {
|
|
87
|
+
if (file == Qnil && !additional_file) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (!RB_TYPE_P(file, T_STRING) || rb_obj_class(file) != rb_cString) {
|
|
91
|
+
context->fast_path_supported = false;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
VALUE relative_file = file;
|
|
96
|
+
VALUE dedup_file = file;
|
|
97
|
+
long relative_len = RSTRING_LEN(file);
|
|
98
|
+
long relative_offset = 0;
|
|
99
|
+
const char *file_ptr = RSTRING_PTR(file);
|
|
100
|
+
long file_len = RSTRING_LEN(file);
|
|
101
|
+
if (memchr(file_ptr, '\0', (size_t)file_len) != NULL) {
|
|
102
|
+
context->fast_path_supported = false;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
bool absolute = file_len > 0 && file_ptr[0] == '/';
|
|
106
|
+
if (absolute) {
|
|
107
|
+
if (file_len <= context->root_len ||
|
|
108
|
+
memcmp(file_ptr, RSTRING_PTR(context->root),
|
|
109
|
+
(size_t)context->root_len) != 0 ||
|
|
110
|
+
file_ptr[context->root_len] != '/') {
|
|
111
|
+
// Absolute additional files outside the immutable repository root are
|
|
112
|
+
// ignored. Primary files are already filtered by the same root.
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
relative_len = file_len - context->root_len - 1;
|
|
117
|
+
if (relative_len == 0) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (context->direct_absolute) {
|
|
121
|
+
relative_offset = context->root_len + 1;
|
|
122
|
+
} else {
|
|
123
|
+
relative_file = rb_str_substr(file, context->root_len + 1, file_len);
|
|
124
|
+
dedup_file = relative_file;
|
|
125
|
+
}
|
|
126
|
+
} else if (!additional_file) {
|
|
127
|
+
// Relative primary paths depend on cwd-to-root Pathname semantics. They
|
|
128
|
+
// are uncommon and retain the authoritative Ruby fallback.
|
|
129
|
+
context->fast_path_supported = false;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (relative_len == 0) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (rb_hash_lookup2(context->seen, dedup_file, Qundef) != Qundef) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
rb_hash_aset(context->seen, dedup_file, Qtrue);
|
|
140
|
+
|
|
141
|
+
int encoding_index = rb_enc_get_index(relative_file);
|
|
142
|
+
bool binary = encoding_index == rb_ascii8bit_encindex();
|
|
143
|
+
if (!binary && encoding_index != rb_utf8_encindex() &&
|
|
144
|
+
encoding_index != rb_usascii_encindex() &&
|
|
145
|
+
(!rb_enc_str_asciicompat_p(relative_file) ||
|
|
146
|
+
!string_bytes_are_ascii(relative_file))) {
|
|
147
|
+
context->fast_path_supported = false;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (relative_len > UINT32_MAX) {
|
|
152
|
+
rb_raise(rb_eArgError, "size of filename is too long to pack: %lu bytes",
|
|
153
|
+
(unsigned long)relative_len);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static const unsigned char filename_entry_prefix[] = {
|
|
157
|
+
0x81, 0xa8, 'f', 'i', 'l', 'e', 'n', 'a', 'm', 'e'};
|
|
158
|
+
rb_str_cat(context->packed, (const char *)filename_entry_prefix,
|
|
159
|
+
sizeof(filename_entry_prefix));
|
|
160
|
+
packed_files_append_string_header(context->packed, (uint32_t)relative_len,
|
|
161
|
+
binary);
|
|
162
|
+
const char *relative_ptr = RSTRING_PTR(relative_file) + relative_offset;
|
|
163
|
+
rb_str_cat(context->packed, relative_ptr, relative_len);
|
|
164
|
+
RB_GC_GUARD(relative_file);
|
|
165
|
+
context->files_count++;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static int pack_primary_file_i(VALUE file, VALUE _value,
|
|
170
|
+
VALUE context_value) {
|
|
171
|
+
struct packed_files_context *context =
|
|
172
|
+
(struct packed_files_context *)context_value;
|
|
173
|
+
return packed_files_append_entry(context, file, false) ? ST_CONTINUE
|
|
174
|
+
: ST_STOP;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static bool packed_file_is_absolute(VALUE file, bool additional_file) {
|
|
178
|
+
if (file == Qnil && !additional_file) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return RB_TYPE_P(file, T_STRING) && rb_obj_class(file) == rb_cString &&
|
|
182
|
+
RSTRING_LEN(file) > 0 && RSTRING_PTR(file)[0] == '/';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static int detect_absolute_primary_file_i(VALUE file, VALUE _value,
|
|
186
|
+
VALUE all_absolute_value) {
|
|
187
|
+
bool *all_absolute = (bool *)all_absolute_value;
|
|
188
|
+
if (!packed_file_is_absolute(file, false)) {
|
|
189
|
+
*all_absolute = false;
|
|
190
|
+
return ST_STOP;
|
|
191
|
+
}
|
|
192
|
+
return ST_CONTINUE;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static void
|
|
196
|
+
packed_files_write_array_header(struct packed_files_context *context) {
|
|
197
|
+
unsigned char header[5];
|
|
198
|
+
size_t header_len;
|
|
199
|
+
if (context->files_count < 16) {
|
|
200
|
+
header[0] = (unsigned char)(0x90U | context->files_count);
|
|
201
|
+
header_len = 1;
|
|
202
|
+
} else if (context->files_count < 65536) {
|
|
203
|
+
header[0] = 0xdc;
|
|
204
|
+
header[1] = (unsigned char)(context->files_count >> 8);
|
|
205
|
+
header[2] = (unsigned char)context->files_count;
|
|
206
|
+
header_len = 3;
|
|
207
|
+
} else {
|
|
208
|
+
header[0] = 0xdd;
|
|
209
|
+
header[1] = (unsigned char)(context->files_count >> 24);
|
|
210
|
+
header[2] = (unsigned char)(context->files_count >> 16);
|
|
211
|
+
header[3] = (unsigned char)(context->files_count >> 8);
|
|
212
|
+
header[4] = (unsigned char)context->files_count;
|
|
213
|
+
header_len = 5;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
long body_len = RSTRING_LEN(context->packed) - 5;
|
|
217
|
+
char *packed_ptr = RSTRING_PTR(context->packed);
|
|
218
|
+
if (header_len != 5) {
|
|
219
|
+
memmove(packed_ptr + header_len, packed_ptr + 5, (size_t)body_len);
|
|
220
|
+
rb_str_resize(context->packed, body_len + (long)header_len);
|
|
221
|
+
packed_ptr = RSTRING_PTR(context->packed);
|
|
222
|
+
}
|
|
223
|
+
memcpy(packed_ptr, header, header_len);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static VALUE file_serialization_pack_files(VALUE module, VALUE primary_files,
|
|
227
|
+
VALUE additional_files,
|
|
228
|
+
VALUE root) {
|
|
229
|
+
Check_Type(primary_files, T_HASH);
|
|
230
|
+
Check_Type(additional_files, T_ARRAY);
|
|
231
|
+
if (!RB_TYPE_P(root, T_STRING) || rb_obj_class(root) != rb_cString ||
|
|
232
|
+
RSTRING_LEN(root) == 0 || !string_bytes_are_ascii(root)) {
|
|
233
|
+
return Qnil;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
bool all_absolute = true;
|
|
237
|
+
rb_hash_foreach(primary_files, detect_absolute_primary_file_i,
|
|
238
|
+
(VALUE)&all_absolute);
|
|
239
|
+
long additional_files_len = RARRAY_LEN(additional_files);
|
|
240
|
+
for (long i = 0; all_absolute && i < additional_files_len; i++) {
|
|
241
|
+
all_absolute =
|
|
242
|
+
packed_file_is_absolute(rb_ary_entry(additional_files, i), true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
struct packed_files_context context = {
|
|
246
|
+
.root = root,
|
|
247
|
+
.seen = rb_hash_new(),
|
|
248
|
+
.packed = rb_str_buf_new(4096),
|
|
249
|
+
.root_len = RSTRING_LEN(root),
|
|
250
|
+
.files_count = 0,
|
|
251
|
+
.direct_absolute = all_absolute,
|
|
252
|
+
.fast_path_supported = true};
|
|
253
|
+
rb_str_resize(context.packed, 5);
|
|
254
|
+
|
|
255
|
+
rb_hash_foreach(primary_files, pack_primary_file_i, (VALUE)&context);
|
|
256
|
+
if (!context.fast_path_supported) {
|
|
257
|
+
return Qnil;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (long i = 0; i < additional_files_len; i++) {
|
|
261
|
+
if (!packed_files_append_entry(&context,
|
|
262
|
+
rb_ary_entry(additional_files, i), true)) {
|
|
263
|
+
return Qnil;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
packed_files_write_array_header(&context);
|
|
268
|
+
return context.packed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
void Init_file_serialization(void) {
|
|
272
|
+
VALUE mDatadog = rb_define_module("Datadog");
|
|
273
|
+
VALUE mCI = rb_define_module_under(mDatadog, "CI");
|
|
274
|
+
VALUE mFileSerialization =
|
|
275
|
+
rb_define_module_under(mCI, "FileSerialization");
|
|
276
|
+
|
|
277
|
+
rb_define_singleton_method(mFileSerialization, "pack_files",
|
|
278
|
+
file_serialization_pack_files, 3);
|
|
279
|
+
}
|
|
@@ -147,6 +147,20 @@ module Datadog
|
|
|
147
147
|
end
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# The intended test process may be reached through an exec'ing launcher
|
|
151
|
+
# such as Bundler, so keep activation until its test session starts.
|
|
152
|
+
# Forked workers then inherit loaded instrumentation, while unrelated
|
|
153
|
+
# Ruby processes started by the suite do not initialize it again.
|
|
154
|
+
def self.remove_auto_instrumentation_from_rubyopt
|
|
155
|
+
rubyopt = ENV["RUBYOPT"]
|
|
156
|
+
return unless rubyopt
|
|
157
|
+
|
|
158
|
+
auto_instrument_require = /(?<!\S)-r\s*datadog\/ci\/auto_instrument(?!\S)/
|
|
159
|
+
rubyopt = rubyopt.gsub(auto_instrument_require, "")
|
|
160
|
+
|
|
161
|
+
rubyopt.match?(/\S/) ? ENV["RUBYOPT"] = rubyopt : ENV.delete("RUBYOPT")
|
|
162
|
+
end
|
|
163
|
+
|
|
150
164
|
def self.auto_configure_datadog
|
|
151
165
|
configure_once.run do
|
|
152
166
|
Datadog.logger.debug("Applying Datadog configuration in CI mode...")
|
|
@@ -18,6 +18,10 @@ module Datadog
|
|
|
18
18
|
return super if @runner != ::ParallelTests::RSpec::Runner
|
|
19
19
|
|
|
20
20
|
begin
|
|
21
|
+
# Preserve activation explicitly for the RSpec worker commands.
|
|
22
|
+
# Starting the distributed parent session removes inherited
|
|
23
|
+
# activation before unrelated child processes can receive it.
|
|
24
|
+
worker_rubyopt = ENV["RUBYOPT"]
|
|
21
25
|
test_session = test_tracing_component.start_test_session(
|
|
22
26
|
tags: {
|
|
23
27
|
CI::Ext::Test::TAG_FRAMEWORK => CI::Contrib::RSpec::Ext::FRAMEWORK,
|
|
@@ -31,6 +35,7 @@ module Datadog
|
|
|
31
35
|
|
|
32
36
|
options[:env] ||= {}
|
|
33
37
|
options[:env][CI::Ext::Settings::ENV_TEST_VISIBILITY_DRB_SERVER_URI] = test_tracing_component.context_service_uri
|
|
38
|
+
options[:env]["RUBYOPT"] ||= worker_rubyopt if worker_rubyopt
|
|
34
39
|
|
|
35
40
|
super
|
|
36
41
|
ensure
|
|
@@ -6,8 +6,8 @@ module Datadog
|
|
|
6
6
|
# PathFilter determines whether a file path should be included in test impact analysis.
|
|
7
7
|
#
|
|
8
8
|
# A path is included if:
|
|
9
|
-
# - It
|
|
10
|
-
# - It
|
|
9
|
+
# - It is equal to root_path or located below it
|
|
10
|
+
# - It is NOT equal to ignored_path or located below it
|
|
11
11
|
#
|
|
12
12
|
# This module mirrors the C implementation in datadog_common.c (dd_ci_is_path_included).
|
|
13
13
|
module PathFilter
|
|
@@ -19,14 +19,22 @@ module Datadog
|
|
|
19
19
|
# @return [Boolean] true if the path should be included
|
|
20
20
|
def self.included?(path, root_path, ignored_path = nil)
|
|
21
21
|
return false unless path.is_a?(String) && root_path.is_a?(String)
|
|
22
|
-
return false unless
|
|
22
|
+
return false unless directory_prefix?(path, root_path)
|
|
23
23
|
|
|
24
24
|
if ignored_path.is_a?(String) && !ignored_path.empty?
|
|
25
|
-
return false if
|
|
25
|
+
return false if directory_prefix?(path, ignored_path)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
true
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
def self.directory_prefix?(path, prefix)
|
|
32
|
+
return false unless path.start_with?(prefix)
|
|
33
|
+
|
|
34
|
+
path.length == prefix.length || prefix.end_with?(File::SEPARATOR) ||
|
|
35
|
+
path[prefix.length] == File::SEPARATOR
|
|
36
|
+
end
|
|
37
|
+
private_class_method :directory_prefix?
|
|
30
38
|
end
|
|
31
39
|
end
|
|
32
40
|
end
|
data/lib/datadog/ci/span.rb
CHANGED
|
@@ -17,6 +17,14 @@ module Datadog
|
|
|
17
17
|
class Span
|
|
18
18
|
include DRb::DRbUndumped
|
|
19
19
|
|
|
20
|
+
ENVIRONMENT_RUNTIME_TAGS = {
|
|
21
|
+
Ext::Test::TAG_OS_ARCHITECTURE => ::RbConfig::CONFIG["host_cpu"],
|
|
22
|
+
Ext::Test::TAG_OS_PLATFORM => ::RbConfig::CONFIG["host_os"],
|
|
23
|
+
Ext::Test::TAG_OS_VERSION => Core::Environment::Platform.kernel_release,
|
|
24
|
+
Ext::Test::TAG_RUNTIME_NAME => Core::Environment::Ext::LANG_ENGINE,
|
|
25
|
+
Ext::Test::TAG_RUNTIME_VERSION => Core::Environment::Ext::ENGINE_VERSION
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
20
28
|
attr_reader :tracer_span
|
|
21
29
|
|
|
22
30
|
def initialize(tracer_span)
|
|
@@ -245,11 +253,7 @@ module Datadog
|
|
|
245
253
|
end
|
|
246
254
|
|
|
247
255
|
def set_environment_runtime_tags
|
|
248
|
-
tracer_span.
|
|
249
|
-
tracer_span.set_tag(Ext::Test::TAG_OS_PLATFORM, ::RbConfig::CONFIG["host_os"])
|
|
250
|
-
tracer_span.set_tag(Ext::Test::TAG_OS_VERSION, Core::Environment::Platform.kernel_release)
|
|
251
|
-
tracer_span.set_tag(Ext::Test::TAG_RUNTIME_NAME, Core::Environment::Ext::LANG_ENGINE)
|
|
252
|
-
tracer_span.set_tag(Ext::Test::TAG_RUNTIME_VERSION, Core::Environment::Ext::ENGINE_VERSION)
|
|
256
|
+
tracer_span.set_tags(ENVIRONMENT_RUNTIME_TAGS)
|
|
253
257
|
tracer_span.set_tag(Ext::Test::TAG_COMMAND, Utils::TestRun.command)
|
|
254
258
|
end
|
|
255
259
|
|
|
@@ -58,12 +58,7 @@ module Datadog
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
packer.write("files")
|
|
61
|
-
|
|
62
|
-
@files.each do |filename|
|
|
63
|
-
packer.write_map_header(1)
|
|
64
|
-
packer.write("filename")
|
|
65
|
-
packer.write(filename)
|
|
66
|
-
end
|
|
61
|
+
@files.write_to(packer)
|
|
67
62
|
end
|
|
68
63
|
|
|
69
64
|
def to_s
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../git/local_repository"
|
|
4
|
+
require_relative "../../file_serialization"
|
|
4
5
|
|
|
5
6
|
module Datadog
|
|
6
7
|
module CI
|
|
@@ -16,6 +17,10 @@ module Datadog
|
|
|
16
17
|
def initialize(coverage, custom_impacted_files = EMPTY_FILES)
|
|
17
18
|
@coverage = coverage
|
|
18
19
|
@custom_impacted_files = custom_impacted_files
|
|
20
|
+
# The repository root is a process invariant between coverage
|
|
21
|
+
# events. Capture it once so native normalization can reuse the
|
|
22
|
+
# same exact boundary for this event without repeated lookups.
|
|
23
|
+
@root = Git::LocalRepository.root
|
|
19
24
|
@normalized_files = nil
|
|
20
25
|
end
|
|
21
26
|
|
|
@@ -27,6 +32,30 @@ module Datadog
|
|
|
27
32
|
normalized_files.size
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
# Writes the complete MessagePack files array. The native fast path
|
|
36
|
+
# combines absolute-path classification, immutable-root slicing,
|
|
37
|
+
# stable deduplication, and filename-entry packing. Any shape it does
|
|
38
|
+
# not support falls back before writing bytes.
|
|
39
|
+
def write_to(packer)
|
|
40
|
+
if FileSerialization.respond_to?(:pack_files) &&
|
|
41
|
+
(packed_files = FileSerialization.pack_files(
|
|
42
|
+
@coverage,
|
|
43
|
+
@custom_impacted_files,
|
|
44
|
+
@root
|
|
45
|
+
))
|
|
46
|
+
packer.buffer.write(packed_files)
|
|
47
|
+
return packer
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
packer.write_array_header(size)
|
|
51
|
+
each do |filename|
|
|
52
|
+
packer.write_map_header(1)
|
|
53
|
+
packer.write("filename")
|
|
54
|
+
packer.write(filename)
|
|
55
|
+
end
|
|
56
|
+
packer
|
|
57
|
+
end
|
|
58
|
+
|
|
30
59
|
# Returns a readable view while preserving the paths supplied by
|
|
31
60
|
# native coverage and the custom impacted-files API. Serialized files
|
|
32
61
|
# are normalized through {#each}.
|
|
@@ -260,6 +260,11 @@ module Datadog
|
|
|
260
260
|
|
|
261
261
|
# DOMAIN EVENTS
|
|
262
262
|
def on_test_session_started(test_session)
|
|
263
|
+
# Forked workers inherit the active test session and instrumentation.
|
|
264
|
+
# Fresh Ruby processes started by the suite should not initialize
|
|
265
|
+
# Test Optimization merely because they inherit RUBYOPT.
|
|
266
|
+
Contrib::Instrumentation.remove_auto_instrumentation_from_rubyopt
|
|
267
|
+
|
|
263
268
|
# signal git tree upload worker to start uploading git metadata
|
|
264
269
|
git_tree_upload_worker.perform(test_session.git_repository_url)
|
|
265
270
|
|
data/lib/datadog/ci/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: datadog-ci
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.37.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Datadog, Inc.
|
|
@@ -79,6 +79,8 @@ files:
|
|
|
79
79
|
- ext/datadog_ci_native/datadog_method_inspect.c
|
|
80
80
|
- ext/datadog_ci_native/datadog_method_inspect.h
|
|
81
81
|
- ext/datadog_ci_native/extconf.rb
|
|
82
|
+
- ext/datadog_ci_native/file_serialization.c
|
|
83
|
+
- ext/datadog_ci_native/file_serialization.h
|
|
82
84
|
- ext/datadog_ci_native/imemo_helpers.c
|
|
83
85
|
- ext/datadog_ci_native/imemo_helpers.h
|
|
84
86
|
- ext/datadog_ci_native/iseq_collector.c
|
|
@@ -210,6 +212,7 @@ files:
|
|
|
210
212
|
- lib/datadog/ci/ext/test_discovery.rb
|
|
211
213
|
- lib/datadog/ci/ext/test_optimization_cache.rb
|
|
212
214
|
- lib/datadog/ci/ext/transport.rb
|
|
215
|
+
- lib/datadog/ci/file_serialization.rb
|
|
213
216
|
- lib/datadog/ci/git/base_branch_sha_detection/base.rb
|
|
214
217
|
- lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb
|
|
215
218
|
- lib/datadog/ci/git/base_branch_sha_detection/guesser.rb
|