datadog-ci 1.25.0 → 1.27.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -2
  3. data/ext/datadog_ci_native/ci.c +5 -3
  4. data/ext/datadog_ci_native/datadog_common.c +64 -0
  5. data/ext/datadog_ci_native/datadog_common.h +60 -0
  6. data/ext/datadog_ci_native/datadog_cov.c +13 -65
  7. data/ext/datadog_ci_native/datadog_method_inspect.c +22 -0
  8. data/ext/datadog_ci_native/datadog_method_inspect.h +4 -0
  9. data/ext/datadog_ci_native/imemo_helpers.c +16 -0
  10. data/ext/datadog_ci_native/imemo_helpers.h +32 -0
  11. data/ext/datadog_ci_native/iseq_collector.c +65 -0
  12. data/ext/datadog_ci_native/iseq_collector.h +6 -0
  13. data/ext/datadog_ci_native/ruby_internal.h +48 -0
  14. data/lib/datadog/ci/code_coverage/component.rb +55 -0
  15. data/lib/datadog/ci/code_coverage/null_component.rb +24 -0
  16. data/lib/datadog/ci/code_coverage/transport.rb +66 -0
  17. data/lib/datadog/ci/configuration/components.rb +19 -2
  18. data/lib/datadog/ci/configuration/settings.rb +26 -2
  19. data/lib/datadog/ci/contrib/minitest/helpers.rb +3 -3
  20. data/lib/datadog/ci/contrib/minitest/parallel_executor_minitest_6.rb +0 -7
  21. data/lib/datadog/ci/contrib/minitest/test.rb +3 -3
  22. data/lib/datadog/ci/contrib/rspec/example.rb +50 -10
  23. data/lib/datadog/ci/contrib/rspec/example_group.rb +63 -31
  24. data/lib/datadog/ci/contrib/simplecov/ext.rb +2 -0
  25. data/lib/datadog/ci/contrib/simplecov/patcher.rb +2 -0
  26. data/lib/datadog/ci/contrib/simplecov/report_uploader.rb +59 -0
  27. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +65 -2
  28. data/lib/datadog/ci/ext/environment.rb +10 -0
  29. data/lib/datadog/ci/ext/settings.rb +3 -0
  30. data/lib/datadog/ci/ext/telemetry.rb +5 -0
  31. data/lib/datadog/ci/ext/test.rb +0 -5
  32. data/lib/datadog/ci/ext/transport.rb +4 -0
  33. data/lib/datadog/ci/git/cli.rb +59 -1
  34. data/lib/datadog/ci/remote/component.rb +6 -1
  35. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  36. data/lib/datadog/ci/source_code/constant_resolver.rb +43 -0
  37. data/lib/datadog/ci/{utils/source_code.rb → source_code/method_inspect.rb} +3 -3
  38. data/lib/datadog/ci/source_code/path_filter.rb +33 -0
  39. data/lib/datadog/ci/source_code/static_dependencies.rb +71 -0
  40. data/lib/datadog/ci/source_code/static_dependencies_extractor.rb +237 -0
  41. data/lib/datadog/ci/test.rb +27 -18
  42. data/lib/datadog/ci/test_management/component.rb +9 -0
  43. data/lib/datadog/ci/test_management/null_component.rb +8 -0
  44. data/lib/datadog/ci/test_optimisation/component.rb +202 -20
  45. data/lib/datadog/ci/test_optimisation/null_component.rb +19 -5
  46. data/lib/datadog/ci/test_suite.rb +16 -21
  47. data/lib/datadog/ci/test_visibility/component.rb +1 -2
  48. data/lib/datadog/ci/test_visibility/known_tests.rb +59 -6
  49. data/lib/datadog/ci/transport/api/agentless.rb +8 -1
  50. data/lib/datadog/ci/transport/api/base.rb +21 -0
  51. data/lib/datadog/ci/transport/api/builder.rb +5 -1
  52. data/lib/datadog/ci/transport/api/evp_proxy.rb +8 -0
  53. data/lib/datadog/ci/version.rb +1 -1
  54. metadata +19 -4
  55. data/ext/datadog_ci_native/datadog_source_code.c +0 -28
  56. data/ext/datadog_ci_native/datadog_source_code.h +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e68ff85fe19330eb395ad72da915dbad1bf98c02f5c917402da73b4f5ebab745
4
- data.tar.gz: aa30fff0bb9f8349e3c9caa5d9ff77cbdd57b7c1cf8e61fdd19d98ebcb271711
3
+ metadata.gz: 47e2dae93f942112cea718e6772f7c586214d97a4d3fb9d8ecf9643826c4e9a0
4
+ data.tar.gz: 80558ee95a33818f80f937981b9c0533c8e638444ac181a31683ea22d53c7ad7
5
5
  SHA512:
6
- metadata.gz: '0668a4e35ef63218065e169ab00dfe7e7bfc4439c35f6020611d35a2a625dbdb20d38aa3e7edd3a5cf3769e15bd31077717141029223637cabeb34b172069aa8'
7
- data.tar.gz: 119fef9be7bd8ab89c013fa545dd3d09b8013fa6ef79db438e59c12b8fd1acc42734bc9faa8f0f3ff63d28fec0b3480c7f43e89ca10a5ce83ee7b6e7a26b1422
6
+ metadata.gz: d442d8921f4ba7fd8ae372544f518edeaac97a6a4b89aa52d2cc8e6ff8035e3131d61104fc4bce7030634970fc2cd52a13d929944ad08497e7abfcf174634ccb
7
+ data.tar.gz: 4f58631b0792099fdff630a65c10f162e3c933ac2e5c32b53addbaebc4533daf4c87ae7112b69abbd4cf113257a4ac1cc58dc59fb2f8b63184ba3f9abb1aec8d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.27.0] - 2026-01-22
4
+
5
+
6
+ ### Added
7
+
8
+ * Add context-level code coverage tracking for RSpec to improve Test Impact Analysis correctness ([#460][])
9
+ * Add automatic uploading of code coverage reports to Datadog Code Coverage product ([#456][])
10
+
11
+ ### Changed
12
+
13
+ * Use pagination for known tests API ([#455][]) ([#458][])
14
+ * GitHub Actions: extract numeric job ID for accurate job URLs ([#457][])
15
+
16
+ ### Fixed
17
+
18
+ * RSpec: take into account Test Management disabled tests when deciding to skip context hooks ([#463][])
19
+ * Fix for attempt-to-fix flow: don't mask failures for flaky tests being fixed ([#464][])
20
+ * Pass -c safe.directory to all git commands to fix dubious ownership errors ([#454][])
21
+ * fix: rename DD_TRACE_CI_ENABLED to DD_CIVISIBILITY_ENABLED with backwards compatibility ([#453][])
22
+
23
+ ## [1.26.0] - 2026-01-09
24
+
25
+ ========== Changelog ==========
26
+ ### Changed
27
+
28
+ * Track constants usage in each Ruby file as a static dependency to enrich Test Impact Analysis data ([#442][])
29
+
3
30
  ## [1.25.0] - 2025-12-19
4
31
 
5
32
  ### Added
@@ -571,7 +598,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
571
598
 
572
599
  - Ruby versions < 2.7 no longer supported ([#8][])
573
600
 
574
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.25.0...main
601
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.27.0...main
602
+ [1.27.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.26.0...v1.27.0
603
+ [1.26.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.25.0...v1.26.0
575
604
  [1.25.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.24.0...v1.25.0
576
605
  [1.24.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.3...v1.24.0
577
606
  [1.23.3]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.2...v1.23.3
@@ -809,5 +838,15 @@ Currently test suite level visibility is not used by our instrumentation: it wil
809
838
  [#433]: https://github.com/DataDog/datadog-ci-rb/issues/433
810
839
  [#436]: https://github.com/DataDog/datadog-ci-rb/issues/436
811
840
  [#440]: https://github.com/DataDog/datadog-ci-rb/issues/440
841
+ [#442]: https://github.com/DataDog/datadog-ci-rb/issues/442
812
842
  [#444]: https://github.com/DataDog/datadog-ci-rb/issues/444
813
- [#446]: https://github.com/DataDog/datadog-ci-rb/issues/446
843
+ [#446]: https://github.com/DataDog/datadog-ci-rb/issues/446
844
+ [#453]: https://github.com/DataDog/datadog-ci-rb/issues/453
845
+ [#454]: https://github.com/DataDog/datadog-ci-rb/issues/454
846
+ [#455]: https://github.com/DataDog/datadog-ci-rb/issues/455
847
+ [#456]: https://github.com/DataDog/datadog-ci-rb/issues/456
848
+ [#457]: https://github.com/DataDog/datadog-ci-rb/issues/457
849
+ [#458]: https://github.com/DataDog/datadog-ci-rb/issues/458
850
+ [#460]: https://github.com/DataDog/datadog-ci-rb/issues/460
851
+ [#463]: https://github.com/DataDog/datadog-ci-rb/issues/463
852
+ [#464]: https://github.com/DataDog/datadog-ci-rb/issues/464
@@ -1,10 +1,12 @@
1
1
  #include "datadog_cov.h"
2
- #include "datadog_source_code.h"
2
+ #include "datadog_method_inspect.h"
3
+ #include "iseq_collector.h"
3
4
 
4
5
  void Init_datadog_ci_native(void) {
5
6
  // Coverage::DDCov
6
7
  Init_datadog_cov();
7
8
 
8
- // Utils::SourceCode
9
- Init_datadog_source_code();
9
+ // SourceCode
10
+ Init_datadog_method_inspect();
11
+ Init_iseq_collector();
10
12
  }
@@ -0,0 +1,64 @@
1
+ #include "datadog_common.h"
2
+ #include <ruby.h>
3
+ #include <string.h>
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) {
9
+ return false;
10
+ }
11
+ if (ignored_path_len > 0 &&
12
+ strncmp(ignored_path, path, ignored_path_len) == 0) {
13
+ return false;
14
+ }
15
+ return true;
16
+ }
17
+
18
+ char *dd_ci_ruby_strndup(const char *str, size_t size) {
19
+ char *dup;
20
+
21
+ dup = xmalloc(size + 1);
22
+ memcpy(dup, str, size);
23
+ dup[size] = '\0';
24
+
25
+ return dup;
26
+ }
27
+
28
+ VALUE dd_ci_rescue_nil(VALUE (*function_to_call_safely)(VALUE),
29
+ VALUE function_to_call_safely_arg) {
30
+ int exception_state;
31
+ // rb_protect sets exception_state to non-zero if an exception occurs
32
+ VALUE result = rb_protect(function_to_call_safely,
33
+ function_to_call_safely_arg, &exception_state);
34
+ if (exception_state != 0) {
35
+ rb_set_errinfo(Qnil); // Clear the exception
36
+ return Qnil;
37
+ }
38
+ return result;
39
+ }
40
+
41
+ VALUE dd_ci_get_const_source_location(VALUE const_name_str) {
42
+ return rb_funcall(rb_cObject, rb_intern("const_source_location"), 1,
43
+ const_name_str);
44
+ }
45
+
46
+ VALUE dd_ci_safely_get_const_source_location(VALUE const_name_str) {
47
+ return dd_ci_rescue_nil(dd_ci_get_const_source_location, const_name_str);
48
+ }
49
+
50
+ VALUE dd_ci_resolve_const_to_file(VALUE const_name_str) {
51
+ VALUE source_location =
52
+ dd_ci_safely_get_const_source_location(const_name_str);
53
+ if (NIL_P(source_location) || !RB_TYPE_P(source_location, T_ARRAY) ||
54
+ RARRAY_LEN(source_location) == 0) {
55
+ return Qnil;
56
+ }
57
+
58
+ VALUE filename = RARRAY_AREF(source_location, 0);
59
+ if (NIL_P(filename) || !RB_TYPE_P(filename, T_STRING)) {
60
+ return Qnil;
61
+ }
62
+
63
+ return filename;
64
+ }
@@ -0,0 +1,60 @@
1
+ #ifndef DATADOG_COMMON_H
2
+ #define DATADOG_COMMON_H
3
+
4
+ #include <ruby.h>
5
+ #include <stdbool.h>
6
+
7
+ /* ---- Path filtering ----------------------------------------------------- */
8
+
9
+ /**
10
+ * Check if a file path is under root_path and not under ignored_path.
11
+ * Returns true if the path should be included, false otherwise.
12
+ *
13
+ * @param path The file path to check
14
+ * @param root_path The root path prefix (required)
15
+ * @param root_path_len Length of root_path
16
+ * @param ignored_path Path prefix to exclude (can be NULL)
17
+ * @param ignored_path_len Length of ignored_path (0 if not set)
18
+ */
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);
22
+
23
+ /* ---- Utility functions -------------------------------------------------- */
24
+
25
+ /**
26
+ * Duplicate a string of given size using Ruby's memory allocator.
27
+ * The returned string is null-terminated.
28
+ */
29
+ char *dd_ci_ruby_strndup(const char *str, size_t size);
30
+
31
+ /**
32
+ * Safe exception handling - equivalent to Ruby's "begin/rescue nil".
33
+ * Calls function_to_call_safely with the given argument and returns Qnil
34
+ * if an exception occurs (clearing the exception state).
35
+ */
36
+ VALUE dd_ci_rescue_nil(VALUE (*function_to_call_safely)(VALUE),
37
+ VALUE function_to_call_safely_arg);
38
+
39
+ /**
40
+ * Get source location for a given constant name string.
41
+ * Calls Object.const_source_location(const_name_str).
42
+ * Returns an array [filename, lineno] or nil if not found.
43
+ */
44
+ VALUE dd_ci_get_const_source_location(VALUE const_name_str);
45
+
46
+ /**
47
+ * Safely get source location for a given constant name string.
48
+ * Returns Qnil if an exception occurs (e.g., for C-defined or anonymous
49
+ * classes).
50
+ */
51
+ VALUE dd_ci_safely_get_const_source_location(VALUE const_name_str);
52
+
53
+ /**
54
+ * Resolve a constant name to its source file path.
55
+ * Returns the filename (String) where the constant is defined, or Qnil if not
56
+ * found.
57
+ */
58
+ VALUE dd_ci_resolve_const_to_file(VALUE const_name_str);
59
+
60
+ #endif /* DATADOG_COMMON_H */
@@ -4,6 +4,8 @@
4
4
 
5
5
  #include <stdbool.h>
6
6
 
7
+ #include "datadog_common.h"
8
+
7
9
  // This is a native extension that collects a list of Ruby files that were
8
10
  // executed during the test run. It is used to optimize the test suite by
9
11
  // running only the tests that are affected by the changes.
@@ -16,35 +18,6 @@ enum threading_mode { single, multi };
16
18
  // functions declarations
17
19
  static void on_newobj_event(VALUE tracepoint_data, void *data);
18
20
 
19
- // utility functions
20
- static char *ruby_strndup(const char *str, size_t size) {
21
- char *dup;
22
-
23
- dup = xmalloc(size + 1);
24
- memcpy(dup, str, size);
25
- dup[size] = '\0';
26
-
27
- return dup;
28
- }
29
-
30
- // Equivalent to Ruby "begin/rescue nil" call, where we call a C function and
31
- // swallow the exception if it occurs - const_source_location often fails with
32
- // exceptions for classes that are defined in C or for anonymous classes.
33
- static VALUE rescue_nil(VALUE (*function_to_call_safely)(VALUE),
34
- VALUE function_to_call_safely_arg) {
35
- int exception_state;
36
- // rb_protect sets exception_state to non-zero if an exception occurs
37
- // see
38
- // https://github.com/ruby/ruby/blob/3219ecf4f659908674f534491d8934ba54e1143d/include/ruby/internal/intern/proc.h#L349
39
- VALUE result = rb_protect(function_to_call_safely,
40
- function_to_call_safely_arg, &exception_state);
41
- if (exception_state != 0) {
42
- return Qnil;
43
- }
44
-
45
- return result;
46
- }
47
-
48
21
  static int mark_key_for_gc_i(st_data_t key, st_data_t _value, st_data_t _data) {
49
22
  VALUE klass = (VALUE)key;
50
23
  // mark klass link for GC as non-movable to avoid changing hashtable's keys
@@ -158,18 +131,9 @@ static VALUE dd_cov_allocate(VALUE klass) {
158
131
  // not in the ignored folder) and adds it to the impacted_files hash.
159
132
  static bool record_impacted_file(struct dd_cov_data *dd_cov_data,
160
133
  VALUE filename) {
161
- char *filename_ptr = RSTRING_PTR(filename);
162
- // if the current filename is not located under the root, we skip it
163
- if (strncmp(dd_cov_data->root, filename_ptr, dd_cov_data->root_len) != 0) {
164
- return false;
165
- }
166
-
167
- // if ignored_path is provided and the current filename is located under the
168
- // ignored_path, we skip it too this is useful for ignoring bundled gems
169
- // location
170
- if (dd_cov_data->ignored_path_len != 0 &&
171
- strncmp(dd_cov_data->ignored_path, filename_ptr,
172
- dd_cov_data->ignored_path_len) == 0) {
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,
136
+ dd_cov_data->ignored_path_len)) {
173
137
  return false;
174
138
  }
175
139
 
@@ -211,25 +175,14 @@ static void on_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id,
211
175
  record_impacted_file(dd_cov_data, filename);
212
176
  }
213
177
 
214
- // Get source location for a given class name
215
- static VALUE get_source_location(VALUE klass_name) {
216
- return rb_funcall(rb_cObject, rb_intern("const_source_location"), 1,
217
- klass_name);
218
- }
219
-
220
- // Get source location for a given class name and swallow any exceptions
221
- static VALUE safely_get_source_location(VALUE klass_name) {
222
- return rescue_nil(get_source_location, klass_name);
223
- }
224
-
225
178
  // Safely get class name, returns Qnil on any error
226
179
  static VALUE safely_get_class_name(VALUE klass) {
227
- return rescue_nil(rb_class_name, klass);
180
+ return dd_ci_rescue_nil(rb_class_name, klass);
228
181
  }
229
182
 
230
183
  // Safely get module ancestors, returns Qnil on any error
231
184
  static VALUE safely_get_mod_ancestors(VALUE klass) {
232
- return rescue_nil(rb_mod_ancestors, klass);
185
+ return dd_ci_rescue_nil(rb_mod_ancestors, klass);
233
186
  }
234
187
 
235
188
  static bool record_impacted_klass(struct dd_cov_data *dd_cov_data,
@@ -239,14 +192,8 @@ static bool record_impacted_klass(struct dd_cov_data *dd_cov_data,
239
192
  return false;
240
193
  }
241
194
 
242
- VALUE source_location = safely_get_source_location(klass_name);
243
- if (source_location == Qnil || !RB_TYPE_P(source_location, T_ARRAY) ||
244
- RARRAY_LEN(source_location) == 0) {
245
- return false;
246
- }
247
-
248
- VALUE filename = RARRAY_AREF(source_location, 0);
249
- if (filename == Qnil || !RB_TYPE_P(filename, T_STRING)) {
195
+ VALUE filename = dd_ci_resolve_const_to_file(klass_name);
196
+ if (filename == Qnil) {
250
197
  return false;
251
198
  }
252
199
 
@@ -349,12 +296,13 @@ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self) {
349
296
 
350
297
  dd_cov_data->threading_mode = threading_mode;
351
298
  dd_cov_data->root_len = RSTRING_LEN(rb_root);
352
- dd_cov_data->root = ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);
299
+ dd_cov_data->root =
300
+ dd_ci_ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);
353
301
 
354
302
  if (RTEST(rb_ignored_path)) {
355
303
  dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
356
- dd_cov_data->ignored_path = ruby_strndup(RSTRING_PTR(rb_ignored_path),
357
- dd_cov_data->ignored_path_len);
304
+ dd_cov_data->ignored_path = dd_ci_ruby_strndup(
305
+ RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
358
306
  }
359
307
 
360
308
  if (rb_allocation_tracing_enabled == Qtrue) {
@@ -0,0 +1,22 @@
1
+ #include <ruby.h>
2
+
3
+ #include "ruby_internal.h"
4
+
5
+ static VALUE last_line_from_iseq(VALUE self, VALUE iseqw) {
6
+ const rb_iseq_t *iseq = rb_iseqw_to_iseq(iseqw);
7
+
8
+ int line;
9
+ rb_iseq_code_location(iseq, NULL, NULL, &line, NULL);
10
+
11
+ return INT2NUM(line);
12
+ }
13
+
14
+ void Init_datadog_method_inspect(void) {
15
+ VALUE mDatadog = rb_define_module("Datadog");
16
+ VALUE mCI = rb_define_module_under(mDatadog, "CI");
17
+ VALUE mSourceCode = rb_define_module_under(mCI, "SourceCode");
18
+ VALUE mMethodInspect = rb_define_module_under(mSourceCode, "MethodInspect");
19
+
20
+ rb_define_singleton_method(mMethodInspect, "_native_last_line_from_iseq",
21
+ last_line_from_iseq, 1);
22
+ }
@@ -0,0 +1,4 @@
1
+ #pragma once
2
+
3
+ void Init_datadog_method_inspect(void);
4
+
@@ -0,0 +1,16 @@
1
+ #include "imemo_helpers.h"
2
+ #include "ruby_internal.h"
3
+
4
+ int dd_ci_imemo_type(VALUE imemo) {
5
+ return (RBASIC(imemo)->flags >> FL_USHIFT) & DD_CI_IMEMO_MASK;
6
+ }
7
+
8
+ bool dd_ci_imemo_iseq_p(VALUE v) {
9
+ if (!rb_objspace_internal_object_p(v))
10
+ return false;
11
+ if (!RB_TYPE_P(v, T_IMEMO))
12
+ return false;
13
+ if (dd_ci_imemo_type(v) != DD_CI_IMEMO_TYPE_ISEQ)
14
+ return false;
15
+ return true;
16
+ }
@@ -0,0 +1,32 @@
1
+ #ifndef IMEMO_HELPERS_H
2
+ #define IMEMO_HELPERS_H
3
+
4
+ #include <ruby.h>
5
+ #include <stdbool.h>
6
+
7
+ /*
8
+ Here we are using the same trick that debug gem uses here:
9
+
10
+ https://github.com/ruby/debug/blob/master/ext/debug/iseq_collector.c
11
+
12
+ These functions allow us to check if the VALUE is an ISeq object
13
+ */
14
+
15
+ /*
16
+ * IMEMO (internal memo) helpers for working with Ruby's internal objects.
17
+ */
18
+
19
+ #define DD_CI_IMEMO_TYPE_ISEQ 7
20
+ #define DD_CI_IMEMO_MASK 0x0f
21
+
22
+ /**
23
+ * Get the IMEMO type from flags.
24
+ */
25
+ int dd_ci_imemo_type(VALUE imemo);
26
+
27
+ /**
28
+ * Check if a VALUE is an internal ISeq object.
29
+ */
30
+ bool dd_ci_imemo_iseq_p(VALUE v);
31
+
32
+ #endif /* IMEMO_HELPERS_H */
@@ -0,0 +1,65 @@
1
+ #include <ruby.h>
2
+
3
+ #include "imemo_helpers.h"
4
+ #include "ruby_internal.h"
5
+
6
+ /*
7
+ * Callback for rb_objspace_each_objects.
8
+ *
9
+ * The callback receives a range of object slots [vstart, vend) with given
10
+ * stride. Each slot may contain a live object, a freed slot, or garbage.
11
+ * We check each slot for ISeq objects and add them to the result array.
12
+ *
13
+ * See:
14
+ * https://github.com/ruby/ruby/blob/c99670d6683fec770271d35c2ae082514b1abce3/gc.c#L3550
15
+ */
16
+ static int collect_iseqs_callback(void *vstart, void *vend, size_t stride,
17
+ void *data) {
18
+ VALUE iseqs_array = (VALUE)data;
19
+
20
+ for (VALUE v = (VALUE)vstart; v != (VALUE)vend; v += stride) {
21
+ if (dd_ci_imemo_iseq_p(v)) {
22
+ VALUE iseq = rb_iseqw_new((void *)v);
23
+ rb_ary_push(iseqs_array, iseq);
24
+ }
25
+ }
26
+ return 0;
27
+ }
28
+
29
+ /*
30
+ * ISeqCollector.collect_iseqs
31
+ *
32
+ * Walk all live objects in the Ruby object space and collect all
33
+ * instruction sequences (ISeqs) into an array.
34
+ *
35
+ * @return [Array<RubyVM::InstructionSequence>] Array of all live ISeqs
36
+ *
37
+ * NOTE:
38
+ * - Only sees ISeqs that still exist (top-level file ISeqs might be GC'd).
39
+ * Method ISeqs usually survive longer.
40
+ * - The returned ISeqs include all types: method bodies, class bodies,
41
+ * blocks, etc.
42
+
43
+ * It is very similar to iseq_collector from debug gem:
44
+ * https://github.com/ruby/debug/blob/master/ext/debug/iseq_collector.c
45
+ */
46
+
47
+ static VALUE iseq_collector_collect(VALUE self) {
48
+ VALUE iseqs_array = rb_ary_new();
49
+
50
+ rb_objspace_each_objects(collect_iseqs_callback, (void *)iseqs_array);
51
+
52
+ return iseqs_array;
53
+ }
54
+
55
+ /* ---- Module initialization ---------------------------------------------- */
56
+
57
+ void Init_iseq_collector(void) {
58
+ VALUE mDatadog = rb_define_module("Datadog");
59
+ VALUE mCI = rb_define_module_under(mDatadog, "CI");
60
+ VALUE mSourceCode = rb_define_module_under(mCI, "SourceCode");
61
+ VALUE mISeqCollector = rb_define_module_under(mSourceCode, "ISeqCollector");
62
+
63
+ rb_define_singleton_method(mISeqCollector, "collect_iseqs",
64
+ iseq_collector_collect, 0);
65
+ }
@@ -0,0 +1,6 @@
1
+ #ifndef ISEQ_COLLECTOR_H
2
+ #define ISEQ_COLLECTOR_H
3
+
4
+ void Init_iseq_collector(void);
5
+
6
+ #endif /* ISEQ_COLLECTOR_H */
@@ -0,0 +1,48 @@
1
+ #ifndef RUBY_INTERNAL_H
2
+ #define RUBY_INTERNAL_H
3
+
4
+ #include <ruby.h>
5
+
6
+ /*
7
+ * Ruby MRI internal functions and structures.
8
+ *
9
+ * These are not part of Ruby's public C API and are resolved via dynamic
10
+ * linking against libruby. They may change between Ruby versions.
11
+ */
12
+
13
+ /* ---- ISeq structures and functions -------------------------------------- */
14
+
15
+ typedef struct rb_iseq_struct rb_iseq_t;
16
+
17
+ /**
18
+ * Convert an ISeq wrapper VALUE to internal rb_iseq_t pointer.
19
+ */
20
+ const rb_iseq_t *rb_iseqw_to_iseq(VALUE iseqw);
21
+
22
+ /**
23
+ * Get code location (line/column info) from an ISeq.
24
+ */
25
+ void rb_iseq_code_location(const rb_iseq_t *iseq, int *first_lineno,
26
+ int *first_column, int *last_lineno,
27
+ int *last_column);
28
+
29
+ /**
30
+ * Wrap an internal iseq pointer as RubyVM::InstructionSequence.
31
+ */
32
+ VALUE rb_iseqw_new(const void *iseq);
33
+
34
+ /* ---- Object space functions --------------------------------------------- */
35
+
36
+ /**
37
+ * Check if an object is internal (not visible to Ruby code).
38
+ */
39
+ int rb_objspace_internal_object_p(VALUE obj);
40
+
41
+ /**
42
+ * Iterate over all objects in the Ruby object space.
43
+ */
44
+ void rb_objspace_each_objects(int (*callback)(void *start, void *end,
45
+ size_t stride, void *data),
46
+ void *data);
47
+
48
+ #endif /* RUBY_INTERNAL_H */
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ext/environment"
4
+ require_relative "transport"
5
+
6
+ module Datadog
7
+ module CI
8
+ module CodeCoverage
9
+ # CodeCoverage component is responsible for uploading code coverage reports
10
+ # to Datadog's Code Coverage product.
11
+ class Component
12
+ COVERAGE_REPORT_TYPE = "coverage_report"
13
+
14
+ attr_reader :enabled
15
+
16
+ def initialize(enabled:, transport:)
17
+ @enabled = enabled
18
+ @transport = transport
19
+ end
20
+
21
+ def configure(library_configuration)
22
+ @enabled &&= library_configuration.coverage_report_upload_enabled?
23
+
24
+ Datadog.logger.debug do
25
+ "[#{self.class.name}] Configured with enabled=#{@enabled}"
26
+ end
27
+ end
28
+
29
+ def upload(serialized_report:, format:)
30
+ return unless @enabled
31
+ return if serialized_report.nil?
32
+
33
+ Datadog.logger.debug { "[#{self.class.name}] Uploading coverage report..." }
34
+
35
+ event = build_event(format)
36
+
37
+ @transport.send_coverage_report(event: event, coverage_report: serialized_report)
38
+ end
39
+
40
+ def shutdown!
41
+ # noop - transport is synchronous
42
+ end
43
+
44
+ private
45
+
46
+ def build_event(format)
47
+ {
48
+ "type" => COVERAGE_REPORT_TYPE,
49
+ "format" => format
50
+ }.merge(Ext::Environment.tags(ENV))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module CodeCoverage
6
+ class NullComponent
7
+ attr_reader :enabled
8
+
9
+ def initialize
10
+ @enabled = false
11
+ end
12
+
13
+ def configure(library_configuration)
14
+ end
15
+
16
+ def upload(serialized_report:, format:)
17
+ end
18
+
19
+ def shutdown!
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end