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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -2
- 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/integration.rb +1 -1
- data/lib/datadog/ci/contrib/minitest/parallel_executor_minitest_6.rb +40 -0
- data/lib/datadog/ci/contrib/minitest/patcher.rb +11 -1
- data/lib/datadog/ci/contrib/minitest/runnable.rb +0 -5
- data/lib/datadog/ci/contrib/minitest/runnable_minitest_6.rb +49 -0
- data/lib/datadog/ci/contrib/minitest/runner.rb +8 -2
- data/lib/datadog/ci/contrib/minitest/test.rb +5 -5
- 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/test_retries/null_component.rb +1 -2
- data/lib/datadog/ci/version.rb +2 -2
- metadata +18 -5
- data/ext/datadog_ci_native/datadog_source_code.c +0 -28
- 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
|
|
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 =
|
|
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 "../../
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 "../../
|
|
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
|
@@ -4,7 +4,7 @@ module Datadog
|
|
|
4
4
|
module CI
|
|
5
5
|
module VERSION
|
|
6
6
|
MAJOR = 1
|
|
7
|
-
MINOR =
|
|
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.
|
|
25
|
+
MAXIMUM_RUBY_VERSION = "4.1"
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|