datadog-ci 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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