datadog-ci 1.25.0 → 1.26.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 +4 -4
- data/CHANGELOG.md +10 -1
- data/ext/datadog_ci_native/ci.c +5 -3
- data/ext/datadog_ci_native/datadog_common.c +64 -0
- data/ext/datadog_ci_native/datadog_common.h +60 -0
- data/ext/datadog_ci_native/datadog_cov.c +13 -65
- data/ext/datadog_ci_native/datadog_method_inspect.c +22 -0
- data/ext/datadog_ci_native/datadog_method_inspect.h +4 -0
- data/ext/datadog_ci_native/imemo_helpers.c +16 -0
- data/ext/datadog_ci_native/imemo_helpers.h +32 -0
- data/ext/datadog_ci_native/iseq_collector.c +65 -0
- data/ext/datadog_ci_native/iseq_collector.h +6 -0
- data/ext/datadog_ci_native/ruby_internal.h +48 -0
- data/lib/datadog/ci/configuration/components.rb +2 -1
- data/lib/datadog/ci/configuration/settings.rb +6 -0
- data/lib/datadog/ci/contrib/minitest/helpers.rb +3 -3
- data/lib/datadog/ci/contrib/minitest/parallel_executor_minitest_6.rb +0 -7
- data/lib/datadog/ci/contrib/minitest/test.rb +2 -2
- data/lib/datadog/ci/contrib/rspec/example.rb +2 -2
- data/lib/datadog/ci/ext/settings.rb +1 -0
- data/lib/datadog/ci/source_code/constant_resolver.rb +43 -0
- data/lib/datadog/ci/{utils/source_code.rb → source_code/method_inspect.rb} +3 -3
- data/lib/datadog/ci/source_code/path_filter.rb +33 -0
- data/lib/datadog/ci/source_code/static_dependencies.rb +71 -0
- data/lib/datadog/ci/source_code/static_dependencies_extractor.rb +237 -0
- data/lib/datadog/ci/test_optimisation/component.rb +34 -4
- data/lib/datadog/ci/version.rb +1 -1
- metadata +15 -4
- data/ext/datadog_ci_native/datadog_source_code.c +0 -28
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1f81920de6f72669d918ab0ea703312868d380d069df7d17f131c0d64e6925a
|
|
4
|
+
data.tar.gz: 90b37b85ef042fea0b15987d98fa5cf35aad89c599c54ad6ef01d7151dee1386
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cd6c023dcd0c239605084ffc9822766ebbde643af95a44bfc2362e8b4a8339cb30ceef649791a1392322de5c0c25ab5d42b80cdbb777ff1f9da1eed717962a7
|
|
7
|
+
data.tar.gz: f5a196eadb710b862e99d59560971ef5dc35bf5ace1a43733558be8bf1f8a467055b61c637d283ec9ed5cd1f511b6c734fb4085e28c518696ed31a0e90fbec14
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.26.0] - 2026-01-09
|
|
4
|
+
|
|
5
|
+
========== Changelog ==========
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
* Track constants usage in each Ruby file as a static dependency to enrich Test Impact Analysis data ([#442][])
|
|
9
|
+
|
|
3
10
|
## [1.25.0] - 2025-12-19
|
|
4
11
|
|
|
5
12
|
### Added
|
|
@@ -571,7 +578,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
|
|
|
571
578
|
|
|
572
579
|
- Ruby versions < 2.7 no longer supported ([#8][])
|
|
573
580
|
|
|
574
|
-
[Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.
|
|
581
|
+
[Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.26.0...main
|
|
582
|
+
[1.26.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.25.0...v1.26.0
|
|
575
583
|
[1.25.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.24.0...v1.25.0
|
|
576
584
|
[1.24.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.3...v1.24.0
|
|
577
585
|
[1.23.3]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.2...v1.23.3
|
|
@@ -809,5 +817,6 @@ Currently test suite level visibility is not used by our instrumentation: it wil
|
|
|
809
817
|
[#433]: https://github.com/DataDog/datadog-ci-rb/issues/433
|
|
810
818
|
[#436]: https://github.com/DataDog/datadog-ci-rb/issues/436
|
|
811
819
|
[#440]: https://github.com/DataDog/datadog-ci-rb/issues/440
|
|
820
|
+
[#442]: https://github.com/DataDog/datadog-ci-rb/issues/442
|
|
812
821
|
[#444]: https://github.com/DataDog/datadog-ci-rb/issues/444
|
|
813
822
|
[#446]: https://github.com/DataDog/datadog-ci-rb/issues/446
|
data/ext/datadog_ci_native/ci.c
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#include "datadog_cov.h"
|
|
2
|
-
#include "
|
|
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
|
-
//
|
|
9
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
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
|
|
243
|
-
if (
|
|
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 =
|
|
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 =
|
|
357
|
-
|
|
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,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,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 */
|
|
@@ -192,7 +192,8 @@ module Datadog
|
|
|
192
192
|
enabled: settings.ci.enabled && settings.ci.itr_enabled,
|
|
193
193
|
bundle_location: settings.ci.itr_code_coverage_excluded_bundle_path,
|
|
194
194
|
use_single_threaded_coverage: settings.ci.itr_code_coverage_use_single_threaded_mode,
|
|
195
|
-
use_allocation_tracing: settings.ci.itr_test_impact_analysis_use_allocation_tracing
|
|
195
|
+
use_allocation_tracing: settings.ci.itr_test_impact_analysis_use_allocation_tracing,
|
|
196
|
+
static_dependencies_tracking_enabled: settings.ci.tia_static_dependencies_tracking_enabled
|
|
196
197
|
)
|
|
197
198
|
end
|
|
198
199
|
|
|
@@ -168,6 +168,12 @@ module Datadog
|
|
|
168
168
|
o.env CI::Ext::Settings::ENV_TEST_DISCOVERY_OUTPUT_PATH
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
option :tia_static_dependencies_tracking_enabled do |o|
|
|
172
|
+
o.type :bool
|
|
173
|
+
o.env CI::Ext::Settings::ENV_TIA_STATIC_DEPENDENCIES_TRACKING_ENABLED
|
|
174
|
+
o.default false
|
|
175
|
+
end
|
|
176
|
+
|
|
171
177
|
define_method(:instrument) do |integration_name, options = {}, &block|
|
|
172
178
|
return unless enabled
|
|
173
179
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../source_code/constant_resolver"
|
|
4
|
+
|
|
3
5
|
module Datadog
|
|
4
6
|
module CI
|
|
5
7
|
module Contrib
|
|
@@ -66,9 +68,7 @@ module Datadog
|
|
|
66
68
|
def self.extract_source_location_from_class(klass)
|
|
67
69
|
return [] if klass.nil? || klass.name.nil?
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
rescue
|
|
71
|
-
[]
|
|
71
|
+
SourceCode::ConstantResolver.safely_get_const_source_location(klass.name) || []
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
end
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../ext/test"
|
|
4
|
-
require_relative "../../git/local_repository"
|
|
5
|
-
require_relative "../../utils/source_code"
|
|
6
|
-
require_relative "../instrumentation"
|
|
7
|
-
require_relative "ext"
|
|
8
|
-
require_relative "helpers"
|
|
9
|
-
|
|
10
3
|
module Datadog
|
|
11
4
|
module CI
|
|
12
5
|
module Contrib
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../ext/test"
|
|
4
4
|
require_relative "../../git/local_repository"
|
|
5
|
-
require_relative "../../
|
|
5
|
+
require_relative "../../source_code/method_inspect"
|
|
6
6
|
require_relative "../instrumentation"
|
|
7
7
|
require_relative "ext"
|
|
8
8
|
require_relative "helpers"
|
|
@@ -38,7 +38,7 @@ module Datadog
|
|
|
38
38
|
# try to find out where test method starts and ends
|
|
39
39
|
test_method = method(name)
|
|
40
40
|
source_file, first_line_number = test_method.source_location
|
|
41
|
-
last_line_number =
|
|
41
|
+
last_line_number = SourceCode::MethodInspect.last_line(test_method)
|
|
42
42
|
|
|
43
43
|
tags[CI::Ext::Test::TAG_SOURCE_FILE] = Git::LocalRepository.relative_to_root(source_file) if source_file
|
|
44
44
|
tags[CI::Ext::Test::TAG_SOURCE_START] = first_line_number.to_s if first_line_number
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../ext/test"
|
|
4
4
|
require_relative "../../git/local_repository"
|
|
5
|
-
require_relative "../../
|
|
5
|
+
require_relative "../../source_code/method_inspect"
|
|
6
6
|
require_relative "../../utils/test_run"
|
|
7
7
|
require_relative "../instrumentation"
|
|
8
8
|
require_relative "ext"
|
|
@@ -36,7 +36,7 @@ module Datadog
|
|
|
36
36
|
CI::Ext::Test::TAG_PARAMETERS => datadog_test_parameters
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
end_line =
|
|
39
|
+
end_line = SourceCode::MethodInspect.last_line(@example_block)
|
|
40
40
|
tags[CI::Ext::Test::TAG_SOURCE_END] = end_line.to_s if end_line
|
|
41
41
|
|
|
42
42
|
test_retries_component.with_retries do
|
|
@@ -29,6 +29,7 @@ module Datadog
|
|
|
29
29
|
ENV_TEST_DISCOVERY_MODE_ENABLED = "DD_TEST_OPTIMIZATION_DISCOVERY_ENABLED"
|
|
30
30
|
ENV_TEST_DISCOVERY_OUTPUT_PATH = "DD_TEST_OPTIMIZATION_DISCOVERY_FILE"
|
|
31
31
|
ENV_AUTO_INSTRUMENTATION_PROVIDER = "DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER"
|
|
32
|
+
ENV_TIA_STATIC_DEPENDENCIES_TRACKING_ENABLED = "DD_TEST_OPTIMIZATION_TIA_STATIC_DEPS_COVERAGE_ENABLED"
|
|
32
33
|
|
|
33
34
|
# Source: https://docs.datadoghq.com/getting_started/site/
|
|
34
35
|
DD_SITE_ALLOWLIST = %w[
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module CI
|
|
5
|
+
module SourceCode
|
|
6
|
+
# ConstantResolver resolves Ruby constant names to their source file locations.
|
|
7
|
+
#
|
|
8
|
+
# This module uses Object.const_source_location to find where a constant is defined.
|
|
9
|
+
# Constants defined in C extensions or built-in Ruby classes have no source location.
|
|
10
|
+
#
|
|
11
|
+
# This module mirrors the C implementation in datadog_common.c (dd_ci_resolve_const_to_file).
|
|
12
|
+
module ConstantResolver
|
|
13
|
+
# Resolve a constant name to its source file path.
|
|
14
|
+
#
|
|
15
|
+
# @param constant_name [String] The fully qualified constant name (e.g., "Foo::Bar::Baz")
|
|
16
|
+
# @return [String, nil] The absolute file path where the constant is defined, or nil if not found
|
|
17
|
+
def self.resolve_path(constant_name)
|
|
18
|
+
return nil unless constant_name.is_a?(String)
|
|
19
|
+
return nil if constant_name.empty?
|
|
20
|
+
|
|
21
|
+
source_location = safely_get_const_source_location(constant_name)
|
|
22
|
+
return nil unless source_location.is_a?(Array) && !source_location.empty?
|
|
23
|
+
|
|
24
|
+
filename = source_location[0]
|
|
25
|
+
return nil unless filename.is_a?(String)
|
|
26
|
+
|
|
27
|
+
filename
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Safely get source location for a constant, returning nil on any exception.
|
|
31
|
+
# This handles cases like anonymous classes, C-defined constants, etc.
|
|
32
|
+
#
|
|
33
|
+
# @param constant_name [String] The constant name to look up
|
|
34
|
+
# @return [Array, nil] The [filename, lineno] array or nil
|
|
35
|
+
def self.safely_get_const_source_location(constant_name)
|
|
36
|
+
Object.const_source_location(constant_name)
|
|
37
|
+
rescue
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Datadog
|
|
4
4
|
module CI
|
|
5
|
-
module
|
|
6
|
-
module
|
|
5
|
+
module SourceCode
|
|
6
|
+
module MethodInspect
|
|
7
7
|
begin
|
|
8
8
|
require "datadog_ci_native.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
|
|
9
9
|
|
|
@@ -22,7 +22,7 @@ module Datadog
|
|
|
22
22
|
return nil unless iseq.is_a?(RubyVM::InstructionSequence)
|
|
23
23
|
# steep:ignore:end
|
|
24
24
|
|
|
25
|
-
# this function is implemented in ext/datadog_ci_native/
|
|
25
|
+
# this function is implemented in ext/datadog_ci_native/datadog_method_inspect.c
|
|
26
26
|
_native_last_line_from_iseq(iseq)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module CI
|
|
5
|
+
module SourceCode
|
|
6
|
+
# PathFilter determines whether a file path should be included in test impact analysis.
|
|
7
|
+
#
|
|
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)
|
|
11
|
+
#
|
|
12
|
+
# This module mirrors the C implementation in datadog_common.c (dd_ci_is_path_included).
|
|
13
|
+
module PathFilter
|
|
14
|
+
# Check if a file path should be included in analysis.
|
|
15
|
+
#
|
|
16
|
+
# @param path [String] The file path to check
|
|
17
|
+
# @param root_path [String] The root path prefix (required)
|
|
18
|
+
# @param ignored_path [String, nil] Path prefix to exclude (optional)
|
|
19
|
+
# @return [Boolean] true if the path should be included
|
|
20
|
+
def self.included?(path, root_path, ignored_path = nil)
|
|
21
|
+
return false unless path.is_a?(String) && root_path.is_a?(String)
|
|
22
|
+
return false unless path.start_with?(root_path)
|
|
23
|
+
|
|
24
|
+
if ignored_path.is_a?(String) && !ignored_path.empty?
|
|
25
|
+
return false if path.start_with?(ignored_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "static_dependencies_extractor"
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module CI
|
|
7
|
+
module SourceCode
|
|
8
|
+
# ISeqCollector provides native access to Ruby's object space
|
|
9
|
+
# for collecting instruction sequences (ISeqs).
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module ISeqCollector
|
|
13
|
+
STATIC_DEPENDENCIES_EXTRACTION_AVAILABLE = begin
|
|
14
|
+
# We support Ruby >= 3.2 even though technically it is possible to support 3.1
|
|
15
|
+
# The issue is that Ruby 3.1 and earlier doesn't have opt_getconstant_path YARV instruction
|
|
16
|
+
# which makes it a lot harder to parse fully qualified constant access.
|
|
17
|
+
#
|
|
18
|
+
# See the PR https://github.com/DataDog/datadog-ci-rb/pull/442 for more context
|
|
19
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2")
|
|
20
|
+
require "datadog_ci_native.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
|
|
21
|
+
true
|
|
22
|
+
else
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
rescue LoadError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Collect all live ISeqs from the Ruby object space.
|
|
30
|
+
# Falls back to empty array if native extension is not available.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<RubyVM::InstructionSequence>] Array of all live ISeqs
|
|
33
|
+
def self.collect
|
|
34
|
+
return [] unless STATIC_DEPENDENCIES_EXTRACTION_AVAILABLE
|
|
35
|
+
|
|
36
|
+
collect_iseqs
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module StaticDependencies
|
|
41
|
+
# Populate the static dependencies map by scanning all live ISeqs.
|
|
42
|
+
#
|
|
43
|
+
# @param root_path [String] Only process files under this path
|
|
44
|
+
# @param ignored_path [String, nil] Exclude files under this path
|
|
45
|
+
# @return [Hash{String => Hash{String => Boolean}}] The dependencies map
|
|
46
|
+
def self.populate!(root_path, ignored_path = nil)
|
|
47
|
+
raise ArgumentError, "root_path must be a String and not nil" if root_path.nil? || !root_path.is_a?(String)
|
|
48
|
+
|
|
49
|
+
extractor = StaticDependenciesExtractor.new(root_path, ignored_path)
|
|
50
|
+
|
|
51
|
+
ISeqCollector.collect.each do |iseq|
|
|
52
|
+
extractor.extract(iseq)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@dependencies_map = extractor.dependencies_map
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Fetch static dependencies for a given file.
|
|
59
|
+
#
|
|
60
|
+
# @param file [String, nil] The file path to look up
|
|
61
|
+
# @return [Hash{String => Boolean}] Dependencies hash or empty hash
|
|
62
|
+
def self.fetch_static_dependencies(file)
|
|
63
|
+
return {} unless @dependencies_map
|
|
64
|
+
return {} if file.nil?
|
|
65
|
+
|
|
66
|
+
@dependencies_map.fetch(file, {})
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path_filter"
|
|
4
|
+
require_relative "constant_resolver"
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module CI
|
|
8
|
+
module SourceCode
|
|
9
|
+
# StaticDependenciesExtractor extracts static constant dependencies from Ruby bytecode.
|
|
10
|
+
#
|
|
11
|
+
# For each ISeq (compiled Ruby code), it:
|
|
12
|
+
# 1. Extracts the source file path
|
|
13
|
+
# 2. Filters by root_path and ignored_path
|
|
14
|
+
# 3. Scans bytecode for constant references
|
|
15
|
+
# 4. Resolves constants to their source file locations
|
|
16
|
+
# 5. Filters dependency paths by root_path and ignored_path
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# extractor = StaticDependenciesExtractor.new("/app", "/app/vendor")
|
|
20
|
+
# iseq = RubyVM::InstructionSequence.of(some_method)
|
|
21
|
+
# extractor.extract(iseq)
|
|
22
|
+
# deps = extractor.dependencies_map
|
|
23
|
+
# # => { "/app/foo.rb" => { "/app/bar.rb" => true } }
|
|
24
|
+
#
|
|
25
|
+
class StaticDependenciesExtractor
|
|
26
|
+
# BytecodeScanner scans Ruby bytecode instructions for constant references.
|
|
27
|
+
#
|
|
28
|
+
# This class traverses the ISeq#to_a representation to find:
|
|
29
|
+
# - :getconstant instructions - simple constant references
|
|
30
|
+
# - :opt_getconstant_path instructions - optimized qualified constant paths
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
class BytecodeScanner
|
|
34
|
+
# Scan an ISeq body for constant references.
|
|
35
|
+
#
|
|
36
|
+
# @param body [Array] The ISeq body array (last element of ISeq#to_a)
|
|
37
|
+
# @return [Array<String>] Array of constant name strings found in the bytecode
|
|
38
|
+
def scan(body)
|
|
39
|
+
return [] unless body.is_a?(Array)
|
|
40
|
+
|
|
41
|
+
constants = []
|
|
42
|
+
scan_value(body, constants)
|
|
43
|
+
constants
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build a qualified constant name from an array of symbols.
|
|
47
|
+
# e.g., [:Foo, :Bar, :Baz] -> "Foo::Bar::Baz"
|
|
48
|
+
#
|
|
49
|
+
# @param symbol_array [Array<Symbol>] Array of constant name symbols
|
|
50
|
+
# @return [String] The qualified constant path string
|
|
51
|
+
def build_constant_path(symbol_array)
|
|
52
|
+
symbol_array
|
|
53
|
+
.select { |part| part.is_a?(Symbol) }
|
|
54
|
+
.map(&:to_s)
|
|
55
|
+
.join("::")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Recursively scan a Ruby value for constant references.
|
|
61
|
+
#
|
|
62
|
+
# @param value [Object] Any Ruby value from the ISeq representation
|
|
63
|
+
# @param constants [Array<String>] Accumulator for found constants
|
|
64
|
+
def scan_value(value, constants)
|
|
65
|
+
case value
|
|
66
|
+
when Array
|
|
67
|
+
scan_array(value, constants)
|
|
68
|
+
when Hash
|
|
69
|
+
scan_hash(value, constants)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Scan an array for instructions and nested values.
|
|
74
|
+
#
|
|
75
|
+
# @param arr [Array] Array to scan
|
|
76
|
+
# @param constants [Array<String>] Accumulator for found constants
|
|
77
|
+
def scan_array(arr, constants)
|
|
78
|
+
handle_instruction(arr, constants)
|
|
79
|
+
|
|
80
|
+
arr.each do |elem|
|
|
81
|
+
scan_value(elem, constants)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Scan a hash for constant references in keys and values.
|
|
86
|
+
#
|
|
87
|
+
# @param hash [Hash] Hash to scan
|
|
88
|
+
# @param constants [Array<String>] Accumulator for found constants
|
|
89
|
+
def scan_hash(hash, constants)
|
|
90
|
+
hash.each do |key, val|
|
|
91
|
+
scan_value(key, constants)
|
|
92
|
+
scan_value(val, constants)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if an array is a bytecode instruction and handle it.
|
|
97
|
+
# Instructions have the form [:instruction_name, ...args].
|
|
98
|
+
#
|
|
99
|
+
# @param arr [Array] Potential instruction array
|
|
100
|
+
# @param constants [Array<String>] Accumulator for found constants
|
|
101
|
+
def handle_instruction(arr, constants)
|
|
102
|
+
return if arr.size < 2
|
|
103
|
+
return unless arr[0].is_a?(Symbol)
|
|
104
|
+
|
|
105
|
+
case arr[0]
|
|
106
|
+
when :getconstant
|
|
107
|
+
handle_getconstant(arr, constants)
|
|
108
|
+
when :opt_getconstant_path
|
|
109
|
+
handle_opt_getconstant_path(arr, constants)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Handle [:getconstant, :CONST_NAME, ...] instruction.
|
|
114
|
+
#
|
|
115
|
+
# @param instruction [Array] The instruction array
|
|
116
|
+
# @param constants [Array<String>] Accumulator for found constants
|
|
117
|
+
def handle_getconstant(instruction, constants)
|
|
118
|
+
const_name = instruction[1]
|
|
119
|
+
return unless const_name.is_a?(Symbol)
|
|
120
|
+
|
|
121
|
+
constants << const_name.to_s
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Handle [:opt_getconstant_path, cache_entry] instruction.
|
|
125
|
+
# The cache entry is an array of symbols: [:Foo, :Bar, :Baz]
|
|
126
|
+
#
|
|
127
|
+
# @param instruction [Array] The instruction array
|
|
128
|
+
# @param constants [Array<String>] Accumulator for found constants
|
|
129
|
+
def handle_opt_getconstant_path(instruction, constants)
|
|
130
|
+
cache_entry = instruction[1]
|
|
131
|
+
return unless cache_entry.is_a?(Array) && !cache_entry.empty?
|
|
132
|
+
|
|
133
|
+
path = build_constant_path(cache_entry)
|
|
134
|
+
constants << path unless path.empty?
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [Hash{String => Hash{String => Boolean}}] Map of source file to dependencies
|
|
139
|
+
attr_reader :dependencies_map
|
|
140
|
+
|
|
141
|
+
# @return [String] Root path prefix for filtering
|
|
142
|
+
attr_reader :root_path
|
|
143
|
+
|
|
144
|
+
# @return [String, nil] Ignored path prefix for exclusion
|
|
145
|
+
attr_reader :ignored_path
|
|
146
|
+
|
|
147
|
+
# Initialize a new StaticDependenciesExtractor.
|
|
148
|
+
#
|
|
149
|
+
# @param root_path [String] Only process files under this path
|
|
150
|
+
# @param ignored_path [String, nil] Exclude files under this path
|
|
151
|
+
def initialize(root_path, ignored_path = nil)
|
|
152
|
+
@root_path = root_path
|
|
153
|
+
@ignored_path = ignored_path
|
|
154
|
+
@dependencies_map = {}
|
|
155
|
+
@bytecode_scanner = BytecodeScanner.new
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Extract constant dependencies from an ISeq.
|
|
159
|
+
#
|
|
160
|
+
# @param iseq [RubyVM::InstructionSequence] The instruction sequence to process
|
|
161
|
+
# @return [void]
|
|
162
|
+
def extract(iseq)
|
|
163
|
+
path = extract_absolute_path(iseq)
|
|
164
|
+
return if path.nil?
|
|
165
|
+
return unless PathFilter.included?(path, root_path, ignored_path)
|
|
166
|
+
|
|
167
|
+
body = extract_body(iseq)
|
|
168
|
+
return if body.nil?
|
|
169
|
+
|
|
170
|
+
deps = get_or_create_deps(path)
|
|
171
|
+
constant_names = @bytecode_scanner.scan(body)
|
|
172
|
+
|
|
173
|
+
constant_names.each do |const_name|
|
|
174
|
+
resolve_and_store_dependency(const_name, deps)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Reset the dependencies map.
|
|
179
|
+
#
|
|
180
|
+
# @return [void]
|
|
181
|
+
def reset
|
|
182
|
+
@dependencies_map = {}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Extract the absolute path from an ISeq.
|
|
188
|
+
# Returns nil for eval'd code (which has no file).
|
|
189
|
+
#
|
|
190
|
+
# @param iseq [RubyVM::InstructionSequence]
|
|
191
|
+
# @return [String, nil]
|
|
192
|
+
def extract_absolute_path(iseq)
|
|
193
|
+
path = iseq.absolute_path
|
|
194
|
+
return nil unless path.is_a?(String)
|
|
195
|
+
|
|
196
|
+
path
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Extract the body array from an ISeq's SimpleDataFormat.
|
|
200
|
+
# The body is the last element of ISeq#to_a.
|
|
201
|
+
#
|
|
202
|
+
# @param iseq [RubyVM::InstructionSequence]
|
|
203
|
+
# @return [Array, nil]
|
|
204
|
+
def extract_body(iseq)
|
|
205
|
+
arr = iseq.to_a
|
|
206
|
+
return nil unless arr.is_a?(Array) && !arr.empty?
|
|
207
|
+
|
|
208
|
+
body = arr[-1]
|
|
209
|
+
return nil unless body.is_a?(Array)
|
|
210
|
+
|
|
211
|
+
body
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get or create dependencies hash for a given path.
|
|
215
|
+
#
|
|
216
|
+
# @param path [String]
|
|
217
|
+
# @return [Hash{String => Boolean}]
|
|
218
|
+
def get_or_create_deps(path)
|
|
219
|
+
@dependencies_map[path] ||= {}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Resolve a constant name to its file and store in dependencies.
|
|
223
|
+
#
|
|
224
|
+
# @param constant_name [String]
|
|
225
|
+
# @param deps [Hash{String => Boolean}]
|
|
226
|
+
# @return [void]
|
|
227
|
+
def resolve_and_store_dependency(constant_name, deps)
|
|
228
|
+
file_path = ConstantResolver.resolve_path(constant_name)
|
|
229
|
+
return if file_path.nil?
|
|
230
|
+
return unless PathFilter.included?(file_path, root_path, ignored_path)
|
|
231
|
+
|
|
232
|
+
deps[file_path] = true
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -10,6 +10,8 @@ require_relative "../ext/dd_test"
|
|
|
10
10
|
|
|
11
11
|
require_relative "../git/local_repository"
|
|
12
12
|
|
|
13
|
+
require_relative "../source_code/static_dependencies"
|
|
14
|
+
|
|
13
15
|
require_relative "../utils/parsing"
|
|
14
16
|
require_relative "../utils/stateful"
|
|
15
17
|
require_relative "../utils/telemetry"
|
|
@@ -40,7 +42,8 @@ module Datadog
|
|
|
40
42
|
enabled: false,
|
|
41
43
|
bundle_location: nil,
|
|
42
44
|
use_single_threaded_coverage: false,
|
|
43
|
-
use_allocation_tracing: true
|
|
45
|
+
use_allocation_tracing: true,
|
|
46
|
+
static_dependencies_tracking_enabled: false
|
|
44
47
|
)
|
|
45
48
|
@enabled = enabled
|
|
46
49
|
@api = api
|
|
@@ -54,6 +57,7 @@ module Datadog
|
|
|
54
57
|
end
|
|
55
58
|
@use_single_threaded_coverage = use_single_threaded_coverage
|
|
56
59
|
@use_allocation_tracing = use_allocation_tracing
|
|
60
|
+
@static_dependencies_tracking_enabled = static_dependencies_tracking_enabled
|
|
57
61
|
|
|
58
62
|
@test_skipping_enabled = false
|
|
59
63
|
@code_coverage_enabled = false
|
|
@@ -82,7 +86,11 @@ module Datadog
|
|
|
82
86
|
# we skip tests, not suites
|
|
83
87
|
test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, Ext::Test::ITR_TEST_SKIPPING_MODE)
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
if @code_coverage_enabled
|
|
90
|
+
load_datadog_cov!
|
|
91
|
+
|
|
92
|
+
populate_static_dependencies_map!
|
|
93
|
+
end
|
|
86
94
|
|
|
87
95
|
# Load component state first, and if successful, skip fetching skippable tests
|
|
88
96
|
# Also try to restore from DDTest cache if available
|
|
@@ -131,11 +139,14 @@ module Datadog
|
|
|
131
139
|
return
|
|
132
140
|
end
|
|
133
141
|
|
|
142
|
+
# cucumber's gherkin files are not covered by the code coverage collector - we add them here explicitly
|
|
134
143
|
test_source_file = test.source_file
|
|
135
|
-
|
|
136
|
-
# cucumber's gherkin files are not covered by the code coverage collector
|
|
137
144
|
ensure_test_source_covered(test_source_file, coverage) unless test_source_file.nil?
|
|
138
145
|
|
|
146
|
+
# if we have static dependencies tracking enabled then we can make the coverage
|
|
147
|
+
# more precise by fetching which files we depend on based on constants usage
|
|
148
|
+
enrich_coverage_with_static_dependencies(coverage)
|
|
149
|
+
|
|
139
150
|
Telemetry.code_coverage_files(coverage.size)
|
|
140
151
|
|
|
141
152
|
event = Coverage::Event.new(
|
|
@@ -323,6 +334,25 @@ module Datadog
|
|
|
323
334
|
@code_coverage_enabled = false
|
|
324
335
|
end
|
|
325
336
|
|
|
337
|
+
def populate_static_dependencies_map!
|
|
338
|
+
return unless @code_coverage_enabled
|
|
339
|
+
return unless @static_dependencies_tracking_enabled
|
|
340
|
+
|
|
341
|
+
Datadog::CI::SourceCode::StaticDependencies.populate!(Git::LocalRepository.root, @bundle_location)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def enrich_coverage_with_static_dependencies(coverage)
|
|
345
|
+
return unless @static_dependencies_tracking_enabled
|
|
346
|
+
|
|
347
|
+
static_dependencies_map = {}
|
|
348
|
+
coverage.keys.each do |file|
|
|
349
|
+
static_dependencies_map.merge!(
|
|
350
|
+
Datadog::CI::SourceCode::StaticDependencies.fetch_static_dependencies(file)
|
|
351
|
+
)
|
|
352
|
+
end
|
|
353
|
+
coverage.merge!(static_dependencies_map)
|
|
354
|
+
end
|
|
355
|
+
|
|
326
356
|
def ensure_test_source_covered(test_source_file, coverage)
|
|
327
357
|
absolute_test_source_file_path = File.join(Git::LocalRepository.root, test_source_file)
|
|
328
358
|
return if coverage.key?(absolute_test_source_file_path)
|
data/lib/datadog/ci/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Datadog, Inc.
|
|
@@ -72,11 +72,18 @@ files:
|
|
|
72
72
|
- README.md
|
|
73
73
|
- exe/ddcirb
|
|
74
74
|
- ext/datadog_ci_native/ci.c
|
|
75
|
+
- ext/datadog_ci_native/datadog_common.c
|
|
76
|
+
- ext/datadog_ci_native/datadog_common.h
|
|
75
77
|
- ext/datadog_ci_native/datadog_cov.c
|
|
76
78
|
- ext/datadog_ci_native/datadog_cov.h
|
|
77
|
-
- ext/datadog_ci_native/
|
|
78
|
-
- ext/datadog_ci_native/
|
|
79
|
+
- ext/datadog_ci_native/datadog_method_inspect.c
|
|
80
|
+
- ext/datadog_ci_native/datadog_method_inspect.h
|
|
79
81
|
- ext/datadog_ci_native/extconf.rb
|
|
82
|
+
- ext/datadog_ci_native/imemo_helpers.c
|
|
83
|
+
- ext/datadog_ci_native/imemo_helpers.h
|
|
84
|
+
- ext/datadog_ci_native/iseq_collector.c
|
|
85
|
+
- ext/datadog_ci_native/iseq_collector.h
|
|
86
|
+
- ext/datadog_ci_native/ruby_internal.h
|
|
80
87
|
- lib/datadog/ci.rb
|
|
81
88
|
- lib/datadog/ci/async_writer.rb
|
|
82
89
|
- lib/datadog/ci/auto_instrument.rb
|
|
@@ -223,6 +230,11 @@ files:
|
|
|
223
230
|
- lib/datadog/ci/remote/library_settings_client.rb
|
|
224
231
|
- lib/datadog/ci/remote/null_component.rb
|
|
225
232
|
- lib/datadog/ci/remote/slow_test_retries.rb
|
|
233
|
+
- lib/datadog/ci/source_code/constant_resolver.rb
|
|
234
|
+
- lib/datadog/ci/source_code/method_inspect.rb
|
|
235
|
+
- lib/datadog/ci/source_code/path_filter.rb
|
|
236
|
+
- lib/datadog/ci/source_code/static_dependencies.rb
|
|
237
|
+
- lib/datadog/ci/source_code/static_dependencies_extractor.rb
|
|
226
238
|
- lib/datadog/ci/span.rb
|
|
227
239
|
- lib/datadog/ci/test.rb
|
|
228
240
|
- lib/datadog/ci/test_discovery/component.rb
|
|
@@ -293,7 +305,6 @@ files:
|
|
|
293
305
|
- lib/datadog/ci/utils/git.rb
|
|
294
306
|
- lib/datadog/ci/utils/parsing.rb
|
|
295
307
|
- lib/datadog/ci/utils/rum.rb
|
|
296
|
-
- lib/datadog/ci/utils/source_code.rb
|
|
297
308
|
- lib/datadog/ci/utils/stateful.rb
|
|
298
309
|
- lib/datadog/ci/utils/telemetry.rb
|
|
299
310
|
- lib/datadog/ci/utils/test_run.rb
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#include <ruby.h>
|
|
2
|
-
|
|
3
|
-
// These structs and functions are not exported by MRI because they are part of
|
|
4
|
-
// the internal API. We declare them here to use them via dynamic linking.
|
|
5
|
-
typedef struct rb_iseq_struct rb_iseq_t;
|
|
6
|
-
const rb_iseq_t *rb_iseqw_to_iseq(VALUE iseqw);
|
|
7
|
-
void rb_iseq_code_location(const rb_iseq_t *, int *first_lineno,
|
|
8
|
-
int *first_column, int *last_lineno,
|
|
9
|
-
int *last_column);
|
|
10
|
-
|
|
11
|
-
static VALUE last_line_from_iseq(VALUE self, VALUE iseqw) {
|
|
12
|
-
const rb_iseq_t *iseq = rb_iseqw_to_iseq(iseqw);
|
|
13
|
-
|
|
14
|
-
int line;
|
|
15
|
-
rb_iseq_code_location(iseq, NULL, NULL, &line, NULL);
|
|
16
|
-
|
|
17
|
-
return INT2NUM(line);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
void Init_datadog_source_code(void) {
|
|
21
|
-
VALUE mDatadog = rb_define_module("Datadog");
|
|
22
|
-
VALUE mCI = rb_define_module_under(mDatadog, "CI");
|
|
23
|
-
VALUE mUtils = rb_define_module_under(mCI, "Utils");
|
|
24
|
-
VALUE mSourceCode = rb_define_module_under(mUtils, "SourceCode");
|
|
25
|
-
|
|
26
|
-
rb_define_singleton_method(mSourceCode, "_native_last_line_from_iseq",
|
|
27
|
-
last_line_from_iseq, 1);
|
|
28
|
-
}
|