datadog-ci 1.24.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -2
  3. data/ext/datadog_ci_native/ci.c +5 -3
  4. data/ext/datadog_ci_native/datadog_common.c +64 -0
  5. data/ext/datadog_ci_native/datadog_common.h +60 -0
  6. data/ext/datadog_ci_native/datadog_cov.c +13 -65
  7. data/ext/datadog_ci_native/datadog_method_inspect.c +22 -0
  8. data/ext/datadog_ci_native/datadog_method_inspect.h +4 -0
  9. data/ext/datadog_ci_native/imemo_helpers.c +16 -0
  10. data/ext/datadog_ci_native/imemo_helpers.h +32 -0
  11. data/ext/datadog_ci_native/iseq_collector.c +65 -0
  12. data/ext/datadog_ci_native/iseq_collector.h +6 -0
  13. data/ext/datadog_ci_native/ruby_internal.h +48 -0
  14. data/lib/datadog/ci/configuration/components.rb +2 -1
  15. data/lib/datadog/ci/configuration/settings.rb +6 -0
  16. data/lib/datadog/ci/contrib/minitest/helpers.rb +3 -3
  17. data/lib/datadog/ci/contrib/minitest/integration.rb +1 -1
  18. data/lib/datadog/ci/contrib/minitest/parallel_executor_minitest_6.rb +40 -0
  19. data/lib/datadog/ci/contrib/minitest/patcher.rb +11 -1
  20. data/lib/datadog/ci/contrib/minitest/runnable.rb +0 -5
  21. data/lib/datadog/ci/contrib/minitest/runnable_minitest_6.rb +49 -0
  22. data/lib/datadog/ci/contrib/minitest/runner.rb +8 -2
  23. data/lib/datadog/ci/contrib/minitest/test.rb +5 -5
  24. data/lib/datadog/ci/contrib/rspec/example.rb +2 -2
  25. data/lib/datadog/ci/ext/settings.rb +1 -0
  26. data/lib/datadog/ci/source_code/constant_resolver.rb +43 -0
  27. data/lib/datadog/ci/{utils/source_code.rb → source_code/method_inspect.rb} +3 -3
  28. data/lib/datadog/ci/source_code/path_filter.rb +33 -0
  29. data/lib/datadog/ci/source_code/static_dependencies.rb +71 -0
  30. data/lib/datadog/ci/source_code/static_dependencies_extractor.rb +237 -0
  31. data/lib/datadog/ci/test_optimisation/component.rb +34 -4
  32. data/lib/datadog/ci/test_retries/null_component.rb +1 -2
  33. data/lib/datadog/ci/version.rb +2 -2
  34. metadata +18 -5
  35. data/ext/datadog_ci_native/datadog_source_code.c +0 -28
  36. data/ext/datadog_ci_native/datadog_source_code.h +0 -3
@@ -0,0 +1,49 @@
1
+ require_relative "helpers"
2
+
3
+ module Datadog
4
+ module CI
5
+ module Contrib
6
+ module Minitest
7
+ module RunnableMinitest6
8
+ def self.included(base)
9
+ base.singleton_class.prepend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def run_suite(*args)
14
+ return super unless datadog_configuration[:enabled]
15
+ return super if Helpers.parallel?(self)
16
+
17
+ test_suite = Helpers.start_test_suite(self)
18
+
19
+ results = super
20
+ return results unless test_suite
21
+
22
+ test_suite.finish
23
+ results
24
+ end
25
+
26
+ def run(klass, method_name, reporter)
27
+ reporter.prerecord klass, method_name
28
+ reporter.record ::Minitest.run_one_method(klass, method_name)
29
+ end
30
+
31
+ private
32
+
33
+ def datadog_configuration
34
+ Datadog.configuration.ci[:minitest]
35
+ end
36
+
37
+ def _dd_test_visibility_component
38
+ Datadog.send(:components).test_visibility
39
+ end
40
+
41
+ def _dd_test_retries_component
42
+ Datadog.send(:components).test_retries
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -35,14 +35,20 @@ module Datadog
35
35
  test_visibility_component.start_test_module(Ext::FRAMEWORK)
36
36
  end
37
37
 
38
+ def old_run_one_method(klass, method_name)
39
+ result = klass.new(method_name).run
40
+ raise "#{klass}#run _must_ return a Result" unless ::Minitest::Result === result
41
+ result
42
+ end
43
+
38
44
  def run_one_method(klass, method_name)
39
- return super unless datadog_configuration[:enabled]
45
+ return old_run_one_method(klass, method_name) unless datadog_configuration[:enabled]
40
46
 
41
47
  # @type var result: untyped
42
48
  result = nil
43
49
 
44
50
  test_retries_component.with_retries do
45
- result = super
51
+ result = old_run_one_method(klass, method_name)
46
52
  end
47
53
 
48
54
  # get the current test suite and mark this method as done, so we can check if all tests were executed
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../../ext/test"
4
4
  require_relative "../../git/local_repository"
5
- require_relative "../../utils/source_code"
5
+ require_relative "../../source_code/method_inspect"
6
6
  require_relative "../instrumentation"
7
7
  require_relative "ext"
8
8
  require_relative "helpers"
@@ -38,13 +38,13 @@ 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 = Utils::SourceCode.last_line(test_method)
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
45
45
  tags[CI::Ext::Test::TAG_SOURCE_END] = last_line_number.to_s if last_line_number
46
46
 
47
- test_span = test_visibility_component.trace_test(
47
+ test_span = _dd_test_visibility_component.trace_test(
48
48
  name,
49
49
  test_suite_name,
50
50
  tags: tags,
@@ -59,7 +59,7 @@ module Datadog
59
59
  end
60
60
 
61
61
  def after_teardown
62
- test_span = test_visibility_component.active_test
62
+ test_span = _dd_test_visibility_component.active_test
63
63
  return super unless test_span
64
64
 
65
65
  finish_with_result(test_span, result_code)
@@ -95,7 +95,7 @@ module Datadog
95
95
  Datadog.configuration.ci[:minitest]
96
96
  end
97
97
 
98
- def test_visibility_component
98
+ def _dd_test_visibility_component
99
99
  Datadog.send(:components).test_visibility
100
100
  end
101
101
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../../ext/test"
4
4
  require_relative "../../git/local_repository"
5
- require_relative "../../utils/source_code"
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 = Utils::SourceCode.last_line(@example_block)
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 Utils
6
- module SourceCode
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/datadog_source_code.c
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
- load_datadog_cov! if @code_coverage_enabled
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)
@@ -13,8 +13,7 @@ module Datadog
13
13
  end
14
14
 
15
15
  def with_retries(&block)
16
- no_action = proc {}
17
- yield no_action
16
+ yield
18
17
  end
19
18
 
20
19
  def reset_retries!
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 24
7
+ MINOR = 26
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
@@ -22,7 +22,7 @@ module Datadog
22
22
  # To allow testing with the next unreleased version of Ruby, the version check is performed
23
23
  # as `< #{MAXIMUM_RUBY_VERSION}`, meaning prereleases of MAXIMUM_RUBY_VERSION are allowed
24
24
  # but not stable MAXIMUM_RUBY_VERSION releases.
25
- MAXIMUM_RUBY_VERSION = "4.0"
25
+ MAXIMUM_RUBY_VERSION = "4.1"
26
26
  end
27
27
  end
28
28
  end