datadog-ci 1.0.0 → 1.0.1

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: 7f96afc61b44202e506967dc7b2d6ec956c02561a99ec7e9eae4dc26902688b7
4
- data.tar.gz: f6d66d038f8c5a5632f6c27ed2a3c70d9f85394fca2c237825041cb523063ec4
3
+ metadata.gz: 73bd9b05aba53e1197420cc0e4a529339b6526e13775467fa326b4949e44043a
4
+ data.tar.gz: 7613c476a2a3f2cf28d7dd99dd53da2fa6c921c9a03de18a6997f25e3fe0f27a
5
5
  SHA512:
6
- metadata.gz: a6a548db92e125ae6fdb23301554efec581f72ca63b9a32384be33162eb90daa79ebf67ccb5834f2347b094f8d131129cd4a6963a817c55beaed83e0cd10b96b
7
- data.tar.gz: e6da7ae7f7607955b206859e8a6c583d65aea03c59d538de2c3c7de41433a0b9942fe48e6d79db9bc7cb6546100b001dfd90b421c7ed392aa6d10e14034ecd08
6
+ metadata.gz: fc66a07d4ccceb71f18b26d290e387b6b403d7efbb46169ced3db9ad0e67e2f29ec163bdaa43a7f7508c6511ca0c68d99257d32d3a25e62b42e00219d7ef44a1
7
+ data.tar.gz: 6fb71c67ecea77fc61238fa7f739192a7670f51d8f4c052d436d2f4d69743f1202f27e27cf84fa76ce74860145d13badb00862c1c03b37d93dc9a33e597afb3d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.1] - 2024-06-11
4
+
5
+ ### Fixed
6
+ * multi threaded code coverage support for datadog_cov ([#189][])
7
+ * code coverage extension fixes and improvements ([#171][])
8
+
3
9
  ## [1.0.0] - 2024-06-06
4
10
 
5
11
 
@@ -261,7 +267,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
261
267
 
262
268
  - Ruby versions < 2.7 no longer supported ([#8][])
263
269
 
264
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0...main
270
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.1...main
271
+ [1.0.1]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0...v1.0.1
265
272
  [1.0.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0.beta6...v1.0.0
266
273
  [1.0.0.beta6]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0.beta5...v1.0.0.beta6
267
274
  [1.0.0.beta5]: https://github.com/DataDog/datadog-ci-rb/compare/v1.0.0.beta4...v1.0.0.beta5
@@ -361,6 +368,7 @@ Currently test suite level visibility is not used by our instrumentation: it wil
361
368
  [#167]: https://github.com/DataDog/datadog-ci-rb/issues/167
362
369
  [#168]: https://github.com/DataDog/datadog-ci-rb/issues/168
363
370
  [#170]: https://github.com/DataDog/datadog-ci-rb/issues/170
371
+ [#171]: https://github.com/DataDog/datadog-ci-rb/issues/171
364
372
  [#172]: https://github.com/DataDog/datadog-ci-rb/issues/172
365
373
  [#173]: https://github.com/DataDog/datadog-ci-rb/issues/173
366
374
  [#174]: https://github.com/DataDog/datadog-ci-rb/issues/174
@@ -368,4 +376,5 @@ Currently test suite level visibility is not used by our instrumentation: it wil
368
376
  [#180]: https://github.com/DataDog/datadog-ci-rb/issues/180
369
377
  [#183]: https://github.com/DataDog/datadog-ci-rb/issues/183
370
378
  [#185]: https://github.com/DataDog/datadog-ci-rb/issues/185
379
+ [#189]: https://github.com/DataDog/datadog-ci-rb/issues/189
371
380
  [#190]: https://github.com/DataDog/datadog-ci-rb/issues/190
@@ -1,55 +1,54 @@
1
1
  #include <ruby.h>
2
2
  #include <ruby/debug.h>
3
3
 
4
- // constants
5
- #define DD_COV_TARGET_FILES 1
6
- #define DD_COV_TARGET_LINES 2
4
+ #define PROFILE_FRAMES_BUFFER_SIZE 1
7
5
 
8
- static int is_prefix(VALUE prefix, const char *str)
6
+ // threading modes
7
+ #define SINGLE_THREADED_COVERAGE_MODE 0
8
+ #define MULTI_THREADED_COVERAGE_MODE 1
9
+
10
+ char *ruby_strndup(const char *str, size_t size)
9
11
  {
10
- if (prefix == Qnil)
11
- {
12
- return 0;
13
- }
12
+ char *dup;
14
13
 
15
- const char *c_prefix = RSTRING_PTR(prefix);
16
- if (c_prefix == NULL)
17
- {
18
- return 0;
19
- }
14
+ dup = xmalloc(size + 1);
15
+ memcpy(dup, str, size);
16
+ dup[size] = '\0';
20
17
 
21
- long prefix_len = RSTRING_LEN(prefix);
22
- if (strncmp(c_prefix, str, prefix_len) == 0)
23
- {
24
- return 1;
25
- }
26
- else
27
- {
28
- return 0;
29
- }
18
+ return dup;
30
19
  }
31
20
 
32
21
  // Data structure
33
22
  struct dd_cov_data
34
23
  {
35
- VALUE root;
36
- VALUE ignored_path;
37
- int mode;
24
+ char *root;
25
+ long root_len;
26
+
27
+ char *ignored_path;
28
+ long ignored_path_len;
29
+
38
30
  VALUE coverage;
31
+
32
+ uintptr_t last_filename_ptr;
33
+
34
+ // for single threaded mode: thread that is being covered
35
+ VALUE th_covered;
36
+
37
+ int threading_mode;
39
38
  };
40
39
 
41
40
  static void dd_cov_mark(void *ptr)
42
41
  {
43
42
  struct dd_cov_data *dd_cov_data = ptr;
44
43
  rb_gc_mark_movable(dd_cov_data->coverage);
45
- rb_gc_mark_movable(dd_cov_data->root);
46
- rb_gc_mark_movable(dd_cov_data->ignored_path);
44
+ rb_gc_mark_movable(dd_cov_data->th_covered);
47
45
  }
48
46
 
49
47
  static void dd_cov_free(void *ptr)
50
48
  {
51
49
  struct dd_cov_data *dd_cov_data = ptr;
52
-
50
+ xfree(dd_cov_data->root);
51
+ xfree(dd_cov_data->ignored_path);
53
52
  xfree(dd_cov_data);
54
53
  }
55
54
 
@@ -57,8 +56,7 @@ static void dd_cov_compact(void *ptr)
57
56
  {
58
57
  struct dd_cov_data *dd_cov_data = ptr;
59
58
  dd_cov_data->coverage = rb_gc_location(dd_cov_data->coverage);
60
- dd_cov_data->root = rb_gc_location(dd_cov_data->root);
61
- dd_cov_data->ignored_path = rb_gc_location(dd_cov_data->ignored_path);
59
+ dd_cov_data->th_covered = rb_gc_location(dd_cov_data->th_covered);
62
60
  }
63
61
 
64
62
  const rb_data_type_t dd_cov_data_type = {
@@ -74,10 +72,15 @@ static VALUE dd_cov_allocate(VALUE klass)
74
72
  {
75
73
  struct dd_cov_data *dd_cov_data;
76
74
  VALUE obj = TypedData_Make_Struct(klass, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
75
+
77
76
  dd_cov_data->coverage = rb_hash_new();
78
- dd_cov_data->root = Qnil;
79
- dd_cov_data->ignored_path = Qnil;
80
- dd_cov_data->mode = DD_COV_TARGET_FILES;
77
+ dd_cov_data->root = NULL;
78
+ dd_cov_data->root_len = 0;
79
+ dd_cov_data->ignored_path = NULL;
80
+ dd_cov_data->ignored_path_len = 0;
81
+ dd_cov_data->last_filename_ptr = 0;
82
+ dd_cov_data->threading_mode = MULTI_THREADED_COVERAGE_MODE;
83
+
81
84
  return obj;
82
85
  }
83
86
 
@@ -85,7 +88,6 @@ static VALUE dd_cov_allocate(VALUE klass)
85
88
  static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self)
86
89
  {
87
90
  VALUE opt;
88
- int mode;
89
91
 
90
92
  rb_scan_args(argc, argv, "10", &opt);
91
93
  VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
@@ -93,113 +95,141 @@ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self)
93
95
  {
94
96
  rb_raise(rb_eArgError, "root is required");
95
97
  }
96
-
97
98
  VALUE rb_ignored_path = rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));
98
99
 
99
- VALUE rb_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("mode")));
100
- if (!RTEST(rb_mode) || rb_mode == ID2SYM(rb_intern("files")))
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")))
101
103
  {
102
- mode = DD_COV_TARGET_FILES;
104
+ threading_mode = MULTI_THREADED_COVERAGE_MODE;
103
105
  }
104
- else if (rb_mode == ID2SYM(rb_intern("lines")))
106
+ else if (rb_threading_mode == ID2SYM(rb_intern("single")))
105
107
  {
106
- mode = DD_COV_TARGET_LINES;
108
+ threading_mode = SINGLE_THREADED_COVERAGE_MODE;
107
109
  }
108
110
  else
109
111
  {
110
- rb_raise(rb_eArgError, "mode is invalid");
112
+ rb_raise(rb_eArgError, "threading mode is invalid");
111
113
  }
112
114
 
113
115
  struct dd_cov_data *dd_cov_data;
114
116
  TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
115
117
 
116
- dd_cov_data->root = rb_root;
117
- dd_cov_data->ignored_path = rb_ignored_path;
118
- dd_cov_data->mode = mode;
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))
123
+ {
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);
126
+ }
119
127
 
120
128
  return Qnil;
121
129
  }
122
130
 
123
- static void dd_cov_update_line_coverage(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass)
131
+ static void dd_cov_update_coverage(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass)
124
132
  {
125
133
  struct dd_cov_data *dd_cov_data;
126
134
  TypedData_Get_Struct(data, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
127
135
 
128
- const char *filename = rb_sourcefile();
129
- if (filename == NULL)
136
+ const char *c_filename = rb_sourcefile();
137
+
138
+ // skip if we cover the same file again
139
+ uintptr_t current_filename_ptr = (uintptr_t)c_filename;
140
+ if (dd_cov_data->last_filename_ptr == current_filename_ptr)
130
141
  {
131
142
  return;
132
143
  }
144
+ dd_cov_data->last_filename_ptr = current_filename_ptr;
133
145
 
134
- // if given filename is not located under the root, we skip it
135
- if (is_prefix(dd_cov_data->root, filename) == 0)
146
+ VALUE top_frame;
147
+ int captured_frames = rb_profile_frames(
148
+ 0 /* stack starting depth */,
149
+ PROFILE_FRAMES_BUFFER_SIZE,
150
+ &top_frame,
151
+ NULL);
152
+
153
+ if (captured_frames != PROFILE_FRAMES_BUFFER_SIZE)
136
154
  {
137
155
  return;
138
156
  }
139
157
 
140
- // if ignored_path is provided and given filename is located under the ignored_path, we skip it too
141
- // this is useful for ignoring bundled gems location
142
- if (RTEST(dd_cov_data->ignored_path) && is_prefix(dd_cov_data->ignored_path, filename) == 1)
158
+ VALUE filename = rb_profile_frame_path(top_frame);
159
+ if (filename == Qnil)
143
160
  {
144
161
  return;
145
162
  }
146
163
 
147
- VALUE rb_str_source_file = rb_str_new2(filename);
148
-
149
- if (dd_cov_data->mode == DD_COV_TARGET_FILES)
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)
150
167
  {
151
- rb_hash_aset(dd_cov_data->coverage, rb_str_source_file, Qtrue);
152
168
  return;
153
169
  }
154
170
 
155
- // this isn't optimized yet, this is a POC to show that lines coverage is possible
156
- // ITR beta is going to use files coverage, we'll get back to this part when
157
- // we need to implement lines coverage
158
- if (dd_cov_data->mode == DD_COV_TARGET_LINES)
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)
159
174
  {
160
- int line_number = rb_sourceline();
161
- if (line_number <= 0)
162
- {
163
- return;
164
- }
165
-
166
- VALUE rb_lines = rb_hash_aref(dd_cov_data->coverage, rb_str_source_file);
167
- if (rb_lines == Qnil)
168
- {
169
- rb_lines = rb_hash_new();
170
- rb_hash_aset(dd_cov_data->coverage, rb_str_source_file, rb_lines);
171
- }
172
-
173
- rb_hash_aset(rb_lines, INT2FIX(line_number), Qtrue);
175
+ return;
174
176
  }
177
+
178
+ rb_hash_aset(dd_cov_data->coverage, filename, Qtrue);
175
179
  }
176
180
 
177
181
  static VALUE dd_cov_start(VALUE self)
178
182
  {
179
- // get current thread
180
- VALUE thval = rb_thread_current();
181
183
 
182
- // add event hook
183
- rb_thread_add_event_hook(thval, dd_cov_update_line_coverage, RUBY_EVENT_LINE, self);
184
+ struct dd_cov_data *dd_cov_data;
185
+ TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
186
+
187
+ if (dd_cov_data->root_len == 0)
188
+ {
189
+ rb_raise(rb_eRuntimeError, "root is required");
190
+ }
191
+
192
+ if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE)
193
+ {
194
+ VALUE thval = rb_thread_current();
195
+ rb_thread_add_event_hook(thval, dd_cov_update_coverage, RUBY_EVENT_LINE, self);
196
+ dd_cov_data->th_covered = thval;
197
+ }
198
+ else
199
+ {
200
+ rb_add_event_hook(dd_cov_update_coverage, RUBY_EVENT_LINE, self);
201
+ }
184
202
 
185
203
  return self;
186
204
  }
187
205
 
188
206
  static VALUE dd_cov_stop(VALUE self)
189
207
  {
190
- // get current thread
191
- VALUE thval = rb_thread_current();
192
- // remove event hook for the current thread
193
- rb_thread_remove_event_hook(thval, dd_cov_update_line_coverage);
194
-
195
208
  struct dd_cov_data *dd_cov_data;
196
209
  TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);
197
210
 
198
- VALUE cov = dd_cov_data->coverage;
211
+ if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE)
212
+ {
213
+ VALUE thval = rb_thread_current();
214
+ if (!rb_equal(thval, dd_cov_data->th_covered))
215
+ {
216
+ rb_raise(rb_eRuntimeError, "Coverage was not started by this thread");
217
+ }
218
+
219
+ rb_thread_remove_event_hook(dd_cov_data->th_covered, dd_cov_update_coverage);
220
+ dd_cov_data->th_covered = Qnil;
221
+ }
222
+ else
223
+ {
224
+ rb_remove_event_hook(dd_cov_update_coverage);
225
+ }
226
+
227
+ VALUE res = dd_cov_data->coverage;
199
228
 
200
229
  dd_cov_data->coverage = rb_hash_new();
230
+ dd_cov_data->last_filename_ptr = 0;
201
231
 
202
- return cov;
232
+ return res;
203
233
  }
204
234
 
205
235
  void Init_datadog_cov(void)
@@ -129,7 +129,8 @@ module Datadog
129
129
  config_tags: custom_configuration_tags,
130
130
  coverage_writer: coverage_writer,
131
131
  enabled: settings.ci.enabled && settings.ci.itr_enabled,
132
- bundle_location: settings.ci.itr_code_coverage_excluded_bundle_path
132
+ bundle_location: settings.ci.itr_code_coverage_excluded_bundle_path,
133
+ use_single_threaded_coverage: settings.ci.itr_code_coverage_use_single_threaded_mode
133
134
  )
134
135
 
135
136
  git_tree_uploader = Git::TreeUploader.new(api: test_visibility_api)
@@ -76,6 +76,12 @@ module Datadog
76
76
  end
77
77
  end
78
78
 
79
+ option :itr_code_coverage_use_single_threaded_mode do |o|
80
+ o.type :bool
81
+ o.env CI::Ext::Settings::ENV_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE
82
+ o.default false
83
+ end
84
+
79
85
  define_method(:instrument) do |integration_name, options = {}, &block|
80
86
  return unless enabled
81
87
 
@@ -13,6 +13,7 @@ module Datadog
13
13
  ENV_ITR_ENABLED = "DD_CIVISIBILITY_ITR_ENABLED"
14
14
  ENV_GIT_METADATA_UPLOAD_ENABLED = "DD_CIVISIBILITY_GIT_METADATA_UPLOAD_ENABLED"
15
15
  ENV_ITR_CODE_COVERAGE_EXCLUDED_BUNDLE_PATH = "DD_CIVISIBILITY_ITR_CODE_COVERAGE_EXCLUDED_BUNDLE_PATH"
16
+ ENV_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE = "DD_CIVISIBILITY_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE"
16
17
 
17
18
  # Source: https://docs.datadoghq.com/getting_started/site/
18
19
  DD_SITE_ALLOWLIST = %w[
@@ -22,6 +22,8 @@ module Datadog
22
22
  DEFAULT_BUFFER_MAX_SIZE = 10_000
23
23
  DEFAULT_SHUTDOWN_TIMEOUT = 60
24
24
 
25
+ DEFAULT_INTERVAL = 3
26
+
25
27
  def initialize(transport:, options: {})
26
28
  @transport = transport
27
29
 
@@ -32,7 +34,7 @@ module Datadog
32
34
  self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
33
35
 
34
36
  # Workers::IntervalLoop settings
35
- self.loop_base_interval = options[:interval] if options.key?(:interval)
37
+ self.loop_base_interval = options[:interval] || DEFAULT_INTERVAL
36
38
  self.loop_back_off_ratio = options[:back_off_ratio] if options.key?(:back_off_ratio)
37
39
  self.loop_back_off_max = options[:back_off_max] if options.key?(:back_off_max)
38
40
 
@@ -31,7 +31,8 @@ module Datadog
31
31
  api: nil,
32
32
  coverage_writer: nil,
33
33
  enabled: false,
34
- bundle_location: nil
34
+ bundle_location: nil,
35
+ use_single_threaded_coverage: false
35
36
  )
36
37
  @enabled = enabled
37
38
  @api = api
@@ -43,6 +44,7 @@ module Datadog
43
44
  else
44
45
  bundle_location
45
46
  end
47
+ @use_single_threaded_coverage = use_single_threaded_coverage
46
48
 
47
49
  @test_skipping_enabled = false
48
50
  @code_coverage_enabled = false
@@ -186,12 +188,15 @@ module Datadog
186
188
  def coverage_collector
187
189
  Thread.current[:dd_coverage_collector] ||= Coverage::DDCov.new(
188
190
  root: Git::LocalRepository.root,
189
- ignored_path: @bundle_location
191
+ ignored_path: @bundle_location,
192
+ threading_mode: code_coverage_mode
190
193
  )
191
194
  end
192
195
 
193
196
  def load_datadog_cov!
194
197
  require "datadog_cov.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
198
+
199
+ Datadog.logger.debug("Loaded Datadog code coverage collector, using coverage mode: #{code_coverage_mode}")
195
200
  rescue LoadError => e
196
201
  Datadog.logger.error("Failed to load coverage collector: #{e}. Code coverage will not be collected.")
197
202
 
@@ -222,6 +227,10 @@ module Datadog
222
227
  Datadog.logger.debug { "Found #{@skippable_tests.count} skippable tests." }
223
228
  Datadog.logger.debug { "ITR correlation ID: #{@correlation_id}" }
224
229
  end
230
+
231
+ def code_coverage_mode
232
+ @use_single_threaded_coverage ? :single : :multi
233
+ end
225
234
  end
226
235
  end
227
236
  end
@@ -3,9 +3,9 @@
3
3
  module Datadog
4
4
  module CI
5
5
  module VERSION
6
- MAJOR = "1"
7
- MINOR = "0"
8
- PATCH = "0"
6
+ MAJOR = 1
7
+ MINOR = 0
8
+ PATCH = 1
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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-06 00:00:00.000000000 Z
11
+ date: 2024-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: datadog