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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70fc5fea2d200dd9654e43d303bc20ca67937ee3d6fdc03ff5595d5c22991424
4
- data.tar.gz: 0ad11c672983e445ef7baed4fcf15af2c4710871b90962076f3075b572383ca7
3
+ metadata.gz: 2a4aa364e13169f17cd0c37cc4dff152b1f8861895569d7d5bdf0a9398d2415f
4
+ data.tar.gz: 900cc26e588995ffe4dde4c32a42aee348593adf521a498d82d138233595861b
5
5
  SHA512:
6
- metadata.gz: c0df4d0e21c6bab928409176b1eee9b401532eac85b9522623c88dd89f54e4a75cb64c3c528cc508357cc77642e3c7e042bc6459edffea8423ef94d48dd6ee4d
7
- data.tar.gz: '07888f1e31d079859c4e3bcfde608dc6c5c56028857f5c4feb58cbdacac4ab7be6a9865076174cf27cc7d6f9d1868e3f4a7cc6a1e014cd74c34982e335151b55'
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.36.1...main
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
@@ -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 dd_ci_is_path_included(const char *path, const char *root_path,
6
- long root_path_len, const char *ignored_path,
7
- long ignored_path_len) {
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
- if (ignored_path_len > 0 &&
12
- strncmp(ignored_path, path, ignored_path_len) == 0) {
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, const char *root_path,
20
- long root_path_len, const char *ignored_path,
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 tracepoint_data, void *data);
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: cache last seen filename pointer to avoid
44
- // unnecessary string comparison if we stay in the same file.
45
- uintptr_t last_filename_ptr;
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
- VALUE object_allocation_tracepoint;
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
- rb_gc_mark_movable(dd_cov_data->object_allocation_tracepoint);
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->last_filename_ptr = 0;
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->object_allocation_tracepoint = Qnil;
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), dd_cov_data->root,
135
- dd_cov_data->root_len, dd_cov_data->ignored_path,
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
- struct dd_cov_data *dd_cov_data;
149
- TypedData_Get_Struct(data, struct dd_cov_data, &dd_cov_data_type,
150
- dd_cov_data);
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->last_filename_ptr == current_filename_ptr) {
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
- record_impacted_klass(dd_cov_data, mod);
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 tracepoint_data, void *data) {
233
- rb_trace_arg_t *tracearg = rb_tracearg_from_tracepoint(tracepoint_data);
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
- // Skip anonymous classes starting with "#<Class".
248
- // it allows us to skip the source location lookup that will always fail
249
- //
250
- // rb_mod_name returns nil for anonymous classes
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
- struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
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
- RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
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->object_allocation_tracepoint = rb_tracepoint_new(
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
- // add object allocation tracepoint
336
- if (dd_cov_data->object_allocation_tracepoint != Qnil) {
337
- rb_tracepoint_enable(dd_cov_data->object_allocation_tracepoint);
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
- // stop object allocation tracepoint
364
- if (dd_cov_data->object_allocation_tracepoint != Qnil) {
365
- rb_tracepoint_disable(dd_cov_data->object_allocation_tracepoint);
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->last_filename_ptr = 0;
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
+ }
@@ -0,0 +1,3 @@
1
+ #pragma once
2
+
3
+ void Init_file_serialization(void);
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ # Native fast path for serializing large lists of file paths.
6
+ # Implementation in ext/datadog_ci_native/file_serialization.c.
7
+ #
8
+ # @internal
9
+ module FileSerialization
10
+ end
11
+ end
12
+ end
@@ -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 starts with root_path (prefix match)
10
- # - It does NOT start with ignored_path (when ignored_path is set)
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 path.start_with?(root_path)
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 path.start_with?(ignored_path)
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
@@ -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.set_tag(Ext::Test::TAG_OS_ARCHITECTURE, ::RbConfig::CONFIG["host_cpu"])
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
- packer.write_array_header(@files.size)
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
 
@@ -4,8 +4,8 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 36
8
- PATCH = 1
7
+ MINOR = 37
8
+ PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
11
11
  # PRE and BUILD above are modified for dev gems during gem build GHA workflow
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.36.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