datadog-ci 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -2
  3. data/ext/datadog_cov/datadog_cov.c +259 -67
  4. data/lib/datadog/ci/configuration/components.rb +149 -80
  5. data/lib/datadog/ci/configuration/settings.rb +6 -0
  6. data/lib/datadog/ci/contrib/cucumber/formatter.rb +13 -9
  7. data/lib/datadog/ci/contrib/minitest/runnable.rb +5 -1
  8. data/lib/datadog/ci/contrib/minitest/runner.rb +6 -2
  9. data/lib/datadog/ci/contrib/minitest/test.rb +7 -3
  10. data/lib/datadog/ci/contrib/rspec/example.rb +6 -2
  11. data/lib/datadog/ci/contrib/rspec/example_group.rb +5 -1
  12. data/lib/datadog/ci/contrib/rspec/knapsack_pro/runner.rb +6 -2
  13. data/lib/datadog/ci/contrib/rspec/runner.rb +6 -2
  14. data/lib/datadog/ci/ext/environment/providers/appveyor.rb +1 -1
  15. data/lib/datadog/ci/ext/environment/providers/aws_code_pipeline.rb +1 -1
  16. data/lib/datadog/ci/ext/environment/providers/azure.rb +1 -1
  17. data/lib/datadog/ci/ext/environment/providers/bitbucket.rb +1 -1
  18. data/lib/datadog/ci/ext/environment/providers/bitrise.rb +1 -1
  19. data/lib/datadog/ci/ext/environment/providers/buddy.rb +1 -1
  20. data/lib/datadog/ci/ext/environment/providers/buildkite.rb +1 -1
  21. data/lib/datadog/ci/ext/environment/providers/circleci.rb +1 -1
  22. data/lib/datadog/ci/ext/environment/providers/codefresh.rb +1 -1
  23. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +1 -1
  24. data/lib/datadog/ci/ext/environment/providers/gitlab.rb +1 -1
  25. data/lib/datadog/ci/ext/environment/providers/jenkins.rb +1 -1
  26. data/lib/datadog/ci/ext/environment/providers/teamcity.rb +1 -1
  27. data/lib/datadog/ci/ext/environment/providers/travis.rb +1 -1
  28. data/lib/datadog/ci/ext/environment.rb +17 -0
  29. data/lib/datadog/ci/ext/settings.rb +1 -0
  30. data/lib/datadog/ci/ext/telemetry.rb +120 -0
  31. data/lib/datadog/ci/git/local_repository.rb +116 -25
  32. data/lib/datadog/ci/git/search_commits.rb +20 -1
  33. data/lib/datadog/ci/git/telemetry.rb +37 -0
  34. data/lib/datadog/ci/git/tree_uploader.rb +7 -0
  35. data/lib/datadog/ci/git/upload_packfile.rb +22 -1
  36. data/lib/datadog/ci/span.rb +3 -3
  37. data/lib/datadog/ci/test.rb +6 -1
  38. data/lib/datadog/ci/test_module.rb +1 -1
  39. data/lib/datadog/ci/{itr/runner.rb → test_optimisation/component.rb} +29 -12
  40. data/lib/datadog/ci/{itr → test_optimisation}/coverage/ddcov.rb +1 -1
  41. data/lib/datadog/ci/{itr → test_optimisation}/coverage/event.rb +1 -1
  42. data/lib/datadog/ci/{itr → test_optimisation}/coverage/transport.rb +11 -2
  43. data/lib/datadog/ci/{itr → test_optimisation}/coverage/writer.rb +1 -1
  44. data/lib/datadog/ci/{itr → test_optimisation}/skippable.rb +25 -1
  45. data/lib/datadog/ci/test_optimisation/telemetry.rb +56 -0
  46. data/lib/datadog/ci/test_session.rb +1 -1
  47. data/lib/datadog/ci/test_suite.rb +1 -1
  48. data/lib/datadog/ci/test_visibility/component.rb +282 -0
  49. data/lib/datadog/ci/test_visibility/{recorder.rb → context.rb} +39 -187
  50. data/lib/datadog/ci/test_visibility/{null_recorder.rb → null_component.rb} +6 -4
  51. data/lib/datadog/ci/test_visibility/{context → store}/global.rb +1 -1
  52. data/lib/datadog/ci/test_visibility/{context → store}/local.rb +1 -1
  53. data/lib/datadog/ci/test_visibility/telemetry.rb +69 -0
  54. data/lib/datadog/ci/test_visibility/transport.rb +9 -10
  55. data/lib/datadog/ci/transport/adapters/net.rb +42 -11
  56. data/lib/datadog/ci/transport/adapters/net_http_client.rb +17 -0
  57. data/lib/datadog/ci/transport/adapters/telemetry_webmock_safe_adapter.rb +28 -0
  58. data/lib/datadog/ci/transport/event_platform_transport.rb +42 -5
  59. data/lib/datadog/ci/transport/http.rb +49 -21
  60. data/lib/datadog/ci/transport/remote_settings_api.rb +39 -1
  61. data/lib/datadog/ci/transport/telemetry.rb +93 -0
  62. data/lib/datadog/ci/utils/identity.rb +20 -0
  63. data/lib/datadog/ci/utils/parsing.rb +2 -1
  64. data/lib/datadog/ci/utils/telemetry.rb +23 -0
  65. data/lib/datadog/ci/version.rb +1 -1
  66. data/lib/datadog/ci.rb +45 -15
  67. metadata +24 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61f6379b9a5ac0c29ff9ee6b1687c1c2add77d6a5031dfd5758eb34a66cc6235
4
- data.tar.gz: a795d8d69513925ee5ea6cf113cb54cec3711fe65ae5e928fcdeb8e1454323af
3
+ metadata.gz: 57da167788887f67dc501a3dc2869c425754fec3cdefa45770af22364003fb20
4
+ data.tar.gz: ed6645a822db10fbae793c5b226b9d41ca4dbea67449f545135f2571be3894c1
5
5
  SHA512:
6
- metadata.gz: 3ea51d0baa70ecb4e66fb49712f4f291582ecb04455b2d32ec1328bb8ea3c112c4c25d80e450db542ee4f29408786b09a82b5f94fb1c7ef8c96b98d301e61e9b
7
- data.tar.gz: 4f066970c2d9ed93001f35f9096cefcf2a4447a86cf7c1d89435853b1da00389c587924304c7a213e56f58f64b191523d202e34f86da535ded271583881411ac
6
+ metadata.gz: f4586bbf8a10b5bf2b262bafefd222ef981624ced261755bd9d43652506400f6b939e5cd3595b7af0c66a193581328937aabe5b9246efd4cb3a0fa088b9e0ccf
7
+ data.tar.gz: 4169cf67306d9d080e264897ed2cffb5103a9b98ca176fea2784ff3293d4b9dd3decd62df3848b2a7ade783f5dfe6925b86fa7d4727b45de51764af5bc6e1b4a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2024-07-30
4
+
5
+ ### Added
6
+
7
+ * Add test_session metric ([#207][])
8
+ * API metrics ([#206][])
9
+ * git commands telemetry ([#205][])
10
+ * implement ITR metrics for internal telemetry ([#204][])
11
+ * Implement code coverage metrics for internal telemetry ([#203][])
12
+ * Implement manual_api_events metric ([#202][])
13
+ * HTTP transport metrics and minor telemetry tweaks ([#201][])
14
+ * Send event_created and event_finished metrics for internal telemetry ([#200][])
15
+
16
+ ## [1.2.0] - 2024-07-16
17
+
18
+ ### Changed
19
+ * Expand test impact analysis with allocation tracing ([#197][])
20
+
3
21
  ## [1.1.0] - 2024-07-01
4
22
 
5
23
  ### Added
@@ -272,7 +290,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
272
290
 
273
291
  - Ruby versions < 2.7 no longer supported ([#8][])
274
292
 
275
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.1.0...main
293
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.3.0...main
294
+ [1.3.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.2.0...v1.3.0
295
+ [1.2.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.1.0...v1.2.0
276
296
  [1.1.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.1...v1.1.0
277
297
  [1.0.1]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0...v1.0.1
278
298
  [1.0.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0.beta6...v1.0.0
@@ -384,4 +404,13 @@ Currently test suite level visibility is not used by our instrumentation: it wil
384
404
  [#185]: https://github.com/DataDog/datadog-ci-rb/issues/185
385
405
  [#189]: https://github.com/DataDog/datadog-ci-rb/issues/189
386
406
  [#190]: https://github.com/DataDog/datadog-ci-rb/issues/190
387
- [#193]: https://github.com/DataDog/datadog-ci-rb/issues/193
407
+ [#193]: https://github.com/DataDog/datadog-ci-rb/issues/193
408
+ [#197]: https://github.com/DataDog/datadog-ci-rb/issues/197
409
+ [#200]: https://github.com/DataDog/datadog-ci-rb/issues/200
410
+ [#201]: https://github.com/DataDog/datadog-ci-rb/issues/201
411
+ [#202]: https://github.com/DataDog/datadog-ci-rb/issues/202
412
+ [#203]: https://github.com/DataDog/datadog-ci-rb/issues/203
413
+ [#204]: https://github.com/DataDog/datadog-ci-rb/issues/204
414
+ [#205]: https://github.com/DataDog/datadog-ci-rb/issues/205
415
+ [#206]: https://github.com/DataDog/datadog-ci-rb/issues/206
416
+ [#207]: https://github.com/DataDog/datadog-ci-rb/issues/207
@@ -1,13 +1,26 @@
1
1
  #include <ruby.h>
2
2
  #include <ruby/debug.h>
3
+ #include <ruby/st.h>
4
+
5
+ #include <stdbool.h>
6
+
7
+ // This is a native extension that collects a list of Ruby files that were executed during the test run.
8
+ // It is used to optimize the test suite by running only the tests that are affected by the changes.
3
9
 
4
10
  #define PROFILE_FRAMES_BUFFER_SIZE 1
5
11
 
6
12
  // threading modes
7
- #define SINGLE_THREADED_COVERAGE_MODE 0
8
- #define MULTI_THREADED_COVERAGE_MODE 1
13
+ enum threading_mode
14
+ {
15
+ single,
16
+ multi
17
+ };
18
+
19
+ // functions declarations
20
+ static void on_newobj_event(VALUE tracepoint_data, void *data);
9
21
 
10
- char *ruby_strndup(const char *str, size_t size)
22
+ // utility functions
23
+ static char *ruby_strndup(const char *str, size_t size)
11
24
  {
12
25
  char *dup;
13
26
 
@@ -18,30 +31,84 @@ char *ruby_strndup(const char *str, size_t size)
18
31
  return dup;
19
32
  }
20
33
 
34
+ // Equivalent to Ruby "begin/rescue nil" call, where we call a C function and
35
+ // swallow the exception if it occurs - const_source_location often fails with
36
+ // exceptions for classes that are defined in C or for anonymous classes.
37
+ static VALUE rescue_nil(VALUE (*function_to_call_safely)(VALUE), VALUE function_to_call_safely_arg)
38
+ {
39
+ int exception_state;
40
+ // rb_protect sets exception_state to non-zero if an exception occurs
41
+ // see https://github.com/ruby/ruby/blob/3219ecf4f659908674f534491d8934ba54e1143d/include/ruby/internal/intern/proc.h#L349
42
+ VALUE result = rb_protect(function_to_call_safely, function_to_call_safely_arg, &exception_state);
43
+ if (exception_state != 0)
44
+ {
45
+ return Qnil;
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ static int mark_key_for_gc_i(st_data_t key, st_data_t _value, st_data_t _data)
52
+ {
53
+ VALUE klass = (VALUE)key;
54
+ // mark klass link for GC as non-movable to avoid changing hashtable's keys
55
+ rb_gc_mark(klass);
56
+ return ST_CONTINUE;
57
+ }
58
+
21
59
  // Data structure
22
60
  struct dd_cov_data
23
61
  {
62
+ // Ruby hash with filenames impacted by the test.
63
+ VALUE impacted_files;
64
+
65
+ // Root is the path to the root folder of the project under test.
66
+ // Files located outside of the root are ignored.
24
67
  char *root;
25
68
  long root_len;
26
69
 
70
+ // Ignored path contains path to the folder where bundled gems are located if
71
+ // gems are installed in the project folder.
27
72
  char *ignored_path;
28
73
  long ignored_path_len;
29
74
 
30
- VALUE coverage;
31
-
75
+ // Line tracepoint optimisation: cache last seen filename pointer to avoid
76
+ // unnecessary string comparison if we stay in the same file.
32
77
  uintptr_t last_filename_ptr;
33
78
 
79
+ // Line tracepoint can work in two modes: single threaded and multi threaded
80
+ //
81
+ // In single threaded mode line tracepoint will only cover the thread that started the coverage.
82
+ // This mode is useful for testing frameworks that run tests in multiple threads.
83
+ // Do not use single threaded mode for Rails applications unless you know that you
84
+ // don't run any background threads.
85
+ //
86
+ // In multi threaded mode line tracepoint will cover all threads. This mode is enabled by default
87
+ // and is recommended for most applications.
88
+ enum threading_mode threading_mode;
34
89
  // for single threaded mode: thread that is being covered
35
90
  VALUE th_covered;
36
91
 
37
- int threading_mode;
92
+ // Allocation tracing is used to track test impact for objects that do not
93
+ // contain any methods that could be covered by line tracepoint.
94
+ //
95
+ // Allocation tracing works only in multi threaded mode.
96
+ VALUE object_allocation_tracepoint;
97
+ st_table *klasses_table; // { (VALUE) -> int } hashmap with class names that were covered by allocation during the test run
38
98
  };
39
99
 
40
100
  static void dd_cov_mark(void *ptr)
41
101
  {
42
102
  struct dd_cov_data *dd_cov_data = ptr;
43
- rb_gc_mark_movable(dd_cov_data->coverage);
103
+ rb_gc_mark_movable(dd_cov_data->impacted_files);
44
104
  rb_gc_mark_movable(dd_cov_data->th_covered);
105
+ rb_gc_mark_movable(dd_cov_data->object_allocation_tracepoint);
106
+
107
+ // if GC starts withing dd_cov_allocate() call, klasses_table might not be initialized yet
108
+ if (dd_cov_data->klasses_table != NULL)
109
+ {
110
+ st_foreach(dd_cov_data->klasses_table, mark_key_for_gc_i, 0);
111
+ }
45
112
  }
46
113
 
47
114
  static void dd_cov_free(void *ptr)
@@ -49,17 +116,20 @@ static void dd_cov_free(void *ptr)
49
116
  struct dd_cov_data *dd_cov_data = ptr;
50
117
  xfree(dd_cov_data->root);
51
118
  xfree(dd_cov_data->ignored_path);
119
+ st_free_table(dd_cov_data->klasses_table);
52
120
  xfree(dd_cov_data);
53
121
  }
54
122
 
55
123
  static void dd_cov_compact(void *ptr)
56
124
  {
57
125
  struct dd_cov_data *dd_cov_data = ptr;
58
- dd_cov_data->coverage = rb_gc_location(dd_cov_data->coverage);
126
+ dd_cov_data->impacted_files = rb_gc_location(dd_cov_data->impacted_files);
59
127
  dd_cov_data->th_covered = rb_gc_location(dd_cov_data->th_covered);
128
+ dd_cov_data->object_allocation_tracepoint = rb_gc_location(dd_cov_data->object_allocation_tracepoint);
129
+ // keys for dd_cov_data->klasses_table are not moved by GC, so we don't need to update them
60
130
  }
61
131
 
62
- const rb_data_type_t dd_cov_data_type = {
132
+ static const rb_data_type_t dd_cov_data_type = {
63
133
  .wrap_struct_name = "dd_cov",
64
134
  .function = {
65
135
  .dmark = dd_cov_mark,
@@ -71,64 +141,48 @@ const rb_data_type_t dd_cov_data_type = {
71
141
  static VALUE dd_cov_allocate(VALUE klass)
72
142
  {
73
143
  struct dd_cov_data *dd_cov_data;
74
- VALUE obj = TypedData_Make_Struct(klass, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
144
+ VALUE dd_cov = TypedData_Make_Struct(klass, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
75
145
 
76
- dd_cov_data->coverage = rb_hash_new();
146
+ dd_cov_data->impacted_files = rb_hash_new();
77
147
  dd_cov_data->root = NULL;
78
148
  dd_cov_data->root_len = 0;
79
149
  dd_cov_data->ignored_path = NULL;
80
150
  dd_cov_data->ignored_path_len = 0;
81
151
  dd_cov_data->last_filename_ptr = 0;
82
- dd_cov_data->threading_mode = MULTI_THREADED_COVERAGE_MODE;
152
+ dd_cov_data->threading_mode = multi;
83
153
 
84
- return obj;
85
- }
154
+ dd_cov_data->object_allocation_tracepoint = Qnil;
155
+ // numtable type is needed to store VALUE as a key
156
+ dd_cov_data->klasses_table = st_init_numtable();
86
157
 
87
- // DDCov methods
88
- static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self)
89
- {
90
- VALUE opt;
158
+ return dd_cov;
159
+ }
91
160
 
92
- rb_scan_args(argc, argv, "10", &opt);
93
- VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
94
- if (!RTEST(rb_root))
95
- {
96
- rb_raise(rb_eArgError, "root is required");
97
- }
98
- VALUE rb_ignored_path = rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));
161
+ // Helper functions (available in C only)
99
162
 
100
- VALUE rb_threading_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode")));
101
- int threading_mode;
102
- if (rb_threading_mode == ID2SYM(rb_intern("multi")))
103
- {
104
- threading_mode = MULTI_THREADED_COVERAGE_MODE;
105
- }
106
- else if (rb_threading_mode == ID2SYM(rb_intern("single")))
107
- {
108
- threading_mode = SINGLE_THREADED_COVERAGE_MODE;
109
- }
110
- else
163
+ // Checks if the filename is located under the root folder of the project (but not
164
+ // in the ignored folder) and adds it to the impacted_files hash.
165
+ static void record_impacted_file(struct dd_cov_data *dd_cov_data, VALUE filename)
166
+ {
167
+ char *filename_ptr = RSTRING_PTR(filename);
168
+ // if the current filename is not located under the root, we skip it
169
+ if (strncmp(dd_cov_data->root, filename_ptr, dd_cov_data->root_len) != 0)
111
170
  {
112
- rb_raise(rb_eArgError, "threading mode is invalid");
171
+ return;
113
172
  }
114
173
 
115
- struct dd_cov_data *dd_cov_data;
116
- TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
117
-
118
- dd_cov_data->threading_mode = threading_mode;
119
- dd_cov_data->root_len = RSTRING_LEN(rb_root);
120
- dd_cov_data->root = ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);
121
-
122
- if (RTEST(rb_ignored_path))
174
+ // if ignored_path is provided and the current filename is located under the ignored_path, we skip it too
175
+ // this is useful for ignoring bundled gems location
176
+ if (dd_cov_data->ignored_path_len != 0 && strncmp(dd_cov_data->ignored_path, filename_ptr, dd_cov_data->ignored_path_len) == 0)
123
177
  {
124
- dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
125
- dd_cov_data->ignored_path = ruby_strndup(RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
178
+ return;
126
179
  }
127
180
 
128
- return Qnil;
181
+ rb_hash_aset(dd_cov_data->impacted_files, filename, Qtrue);
129
182
  }
130
183
 
131
- static void dd_cov_update_coverage(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass)
184
+ // Executed on RUBY_EVENT_LINE event and captures the filename from rb_profile_frames.
185
+ static void on_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass)
132
186
  {
133
187
  struct dd_cov_data *dd_cov_data;
134
188
  TypedData_Get_Struct(data, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
@@ -161,54 +215,182 @@ static void dd_cov_update_coverage(rb_event_flag_t event, VALUE data, VALUE self
161
215
  return;
162
216
  }
163
217
 
164
- char *filename_ptr = RSTRING_PTR(filename);
165
- // if the current filename is not located under the root, we skip it
166
- if (strncmp(dd_cov_data->root, filename_ptr, dd_cov_data->root_len) != 0)
218
+ record_impacted_file(dd_cov_data, filename);
219
+ }
220
+
221
+ // Get source location for a given class name
222
+ static VALUE get_source_location(VALUE klass_name)
223
+ {
224
+ return rb_funcall(rb_cObject, rb_intern("const_source_location"), 1, klass_name);
225
+ }
226
+
227
+ // Get source location for a given class name and swallow any exceptions
228
+ static VALUE safely_get_source_location(VALUE klass_name)
229
+ {
230
+ return rescue_nil(get_source_location, klass_name);
231
+ }
232
+
233
+ // This function is called for each class that was instantiated during the test run.
234
+ static int process_instantiated_klass(st_data_t key, st_data_t _value, st_data_t data)
235
+ {
236
+ VALUE klass = (VALUE)key;
237
+ struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
238
+
239
+ VALUE klass_name = rb_class_name(klass);
240
+ if (klass_name == Qnil)
241
+ {
242
+ return ST_CONTINUE;
243
+ }
244
+
245
+ VALUE source_location = safely_get_source_location(klass_name);
246
+ if (source_location == Qnil || RARRAY_LEN(source_location) == 0)
247
+ {
248
+ return ST_CONTINUE;
249
+ }
250
+
251
+ VALUE filename = RARRAY_AREF(source_location, 0);
252
+ if (filename == Qnil)
253
+ {
254
+ return ST_CONTINUE;
255
+ }
256
+
257
+ record_impacted_file(dd_cov_data, filename);
258
+ return ST_CONTINUE;
259
+ }
260
+
261
+ // Executed on RUBY_INTERNAL_EVENT_NEWOBJ event and captures the source file for the
262
+ // allocated object's class.
263
+ static void on_newobj_event(VALUE tracepoint_data, void *data)
264
+ {
265
+ rb_trace_arg_t *tracearg = rb_tracearg_from_tracepoint(tracepoint_data);
266
+ VALUE new_object = rb_tracearg_object(tracearg);
267
+
268
+ // To keep things fast and practical, we only care about objects that extend
269
+ // either Object or Struct.
270
+ enum ruby_value_type type = rb_type(new_object);
271
+ if (type != RUBY_T_OBJECT && type != RUBY_T_STRUCT)
167
272
  {
168
273
  return;
169
274
  }
170
275
 
171
- // if ignored_path is provided and the current filename is located under the ignored_path, we skip it too
172
- // this is useful for ignoring bundled gems location
173
- if (dd_cov_data->ignored_path_len != 0 && strncmp(dd_cov_data->ignored_path, filename_ptr, dd_cov_data->ignored_path_len) == 0)
276
+ VALUE klass = rb_class_of(new_object);
277
+ if (klass == Qnil || klass == 0)
278
+ {
279
+ return;
280
+ }
281
+ // Skip anonymous classes starting with "#<Class".
282
+ // it allows us to skip the source location lookup that will always fail
283
+ //
284
+ // rb_mod_name returns nil for anonymous classes
285
+ if (rb_mod_name(klass) == Qnil)
174
286
  {
175
287
  return;
176
288
  }
177
289
 
178
- rb_hash_aset(dd_cov_data->coverage, filename, Qtrue);
290
+ struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
291
+
292
+ // We use VALUE directly as a key for the hashmap
293
+ // Ruby itself does it too:
294
+ // https://github.com/ruby/ruby/blob/94b87084a689a3bc732dcaee744508a708223d6c/ext/objspace/object_tracing.c#L113
295
+ st_insert(dd_cov_data->klasses_table, (st_data_t)klass, 1);
179
296
  }
180
297
 
181
- static VALUE dd_cov_start(VALUE self)
298
+ // DDCov instance methods available in Ruby
299
+ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self)
182
300
  {
301
+ VALUE opt;
302
+
303
+ rb_scan_args(argc, argv, "10", &opt);
304
+ VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
305
+ if (!RTEST(rb_root))
306
+ {
307
+ rb_raise(rb_eArgError, "root is required");
308
+ }
309
+ VALUE rb_ignored_path = rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));
310
+
311
+ VALUE rb_threading_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode")));
312
+ enum threading_mode threading_mode;
313
+ if (rb_threading_mode == ID2SYM(rb_intern("multi")))
314
+ {
315
+ threading_mode = multi;
316
+ }
317
+ else if (rb_threading_mode == ID2SYM(rb_intern("single")))
318
+ {
319
+ threading_mode = single;
320
+ }
321
+ else
322
+ {
323
+ rb_raise(rb_eArgError, "threading mode is invalid");
324
+ }
325
+
326
+ VALUE rb_allocation_tracing_enabled = rb_hash_lookup(opt, ID2SYM(rb_intern("use_allocation_tracing")));
327
+ if (rb_allocation_tracing_enabled == Qtrue && threading_mode == single)
328
+ {
329
+ rb_raise(rb_eArgError, "allocation tracing is not supported in single threaded mode");
330
+ }
183
331
 
184
332
  struct dd_cov_data *dd_cov_data;
185
333
  TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
186
334
 
335
+ dd_cov_data->threading_mode = threading_mode;
336
+ dd_cov_data->root_len = RSTRING_LEN(rb_root);
337
+ dd_cov_data->root = ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);
338
+
339
+ if (RTEST(rb_ignored_path))
340
+ {
341
+ dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
342
+ dd_cov_data->ignored_path = ruby_strndup(RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
343
+ }
344
+
345
+ if (rb_allocation_tracing_enabled == Qtrue)
346
+ {
347
+ dd_cov_data->object_allocation_tracepoint = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_NEWOBJ, on_newobj_event, (void *)dd_cov_data);
348
+ }
349
+
350
+ return Qnil;
351
+ }
352
+
353
+ // starts test impact collection, executed before the start of each test
354
+ static VALUE dd_cov_start(VALUE self)
355
+ {
356
+ struct dd_cov_data *dd_cov_data;
357
+ TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
358
+
187
359
  if (dd_cov_data->root_len == 0)
188
360
  {
189
361
  rb_raise(rb_eRuntimeError, "root is required");
190
362
  }
191
363
 
192
- if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE)
364
+ // add line tracepoint
365
+ if (dd_cov_data->threading_mode == single)
193
366
  {
194
367
  VALUE thval = rb_thread_current();
195
- rb_thread_add_event_hook(thval, dd_cov_update_coverage, RUBY_EVENT_LINE, self);
368
+ rb_thread_add_event_hook(thval, on_line_event, RUBY_EVENT_LINE, self);
196
369
  dd_cov_data->th_covered = thval;
197
370
  }
198
371
  else
199
372
  {
200
- rb_add_event_hook(dd_cov_update_coverage, RUBY_EVENT_LINE, self);
373
+ rb_add_event_hook(on_line_event, RUBY_EVENT_LINE, self);
374
+ }
375
+
376
+ // add object allocation tracepoint
377
+ if (dd_cov_data->object_allocation_tracepoint != Qnil)
378
+ {
379
+ rb_tracepoint_enable(dd_cov_data->object_allocation_tracepoint);
201
380
  }
202
381
 
203
382
  return self;
204
383
  }
205
384
 
385
+ // stops test impact collection, executed after the end of each test
386
+ // returns the hash with impacted files and resets the internal state
206
387
  static VALUE dd_cov_stop(VALUE self)
207
388
  {
208
389
  struct dd_cov_data *dd_cov_data;
209
390
  TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
210
391
 
211
- if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE)
392
+ // stop line tracepoint
393
+ if (dd_cov_data->threading_mode == single)
212
394
  {
213
395
  VALUE thval = rb_thread_current();
214
396
  if (!rb_equal(thval, dd_cov_data->th_covered))
@@ -216,17 +398,27 @@ static VALUE dd_cov_stop(VALUE self)
216
398
  rb_raise(rb_eRuntimeError, "Coverage was not started by this thread");
217
399
  }
218
400
 
219
- rb_thread_remove_event_hook(dd_cov_data->th_covered, dd_cov_update_coverage);
401
+ rb_thread_remove_event_hook(dd_cov_data->th_covered, on_line_event);
220
402
  dd_cov_data->th_covered = Qnil;
221
403
  }
222
404
  else
223
405
  {
224
- rb_remove_event_hook(dd_cov_update_coverage);
406
+ rb_remove_event_hook(on_line_event);
225
407
  }
226
408
 
227
- VALUE res = dd_cov_data->coverage;
409
+ // stop object allocation tracepoint
410
+ if (dd_cov_data->object_allocation_tracepoint != Qnil)
411
+ {
412
+ rb_tracepoint_disable(dd_cov_data->object_allocation_tracepoint);
413
+ }
414
+
415
+ // process classes covered by allocation tracing
416
+ st_foreach(dd_cov_data->klasses_table, process_instantiated_klass, (st_data_t)dd_cov_data);
417
+ st_clear(dd_cov_data->klasses_table);
418
+
419
+ VALUE res = dd_cov_data->impacted_files;
228
420
 
229
- dd_cov_data->coverage = rb_hash_new();
421
+ dd_cov_data->impacted_files = rb_hash_new();
230
422
  dd_cov_data->last_filename_ptr = 0;
231
423
 
232
424
  return res;
@@ -236,8 +428,8 @@ void Init_datadog_cov(void)
236
428
  {
237
429
  VALUE mDatadog = rb_define_module("Datadog");
238
430
  VALUE mCI = rb_define_module_under(mDatadog, "CI");
239
- VALUE mITR = rb_define_module_under(mCI, "ITR");
240
- VALUE mCoverage = rb_define_module_under(mITR, "Coverage");
431
+ VALUE mTestOptimisation = rb_define_module_under(mCI, "TestOptimisation");
432
+ VALUE mCoverage = rb_define_module_under(mTestOptimisation, "Coverage");
241
433
  VALUE cDatadogCov = rb_define_class_under(mCoverage, "DDCov", rb_cObject);
242
434
 
243
435
  rb_define_alloc_func(cDatadogCov, dd_cov_allocate);