datadog-ci 1.25.0 → 1.27.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 +41 -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/code_coverage/component.rb +55 -0
- data/lib/datadog/ci/code_coverage/null_component.rb +24 -0
- data/lib/datadog/ci/code_coverage/transport.rb +66 -0
- data/lib/datadog/ci/configuration/components.rb +19 -2
- data/lib/datadog/ci/configuration/settings.rb +26 -2
- 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 +3 -3
- data/lib/datadog/ci/contrib/rspec/example.rb +50 -10
- data/lib/datadog/ci/contrib/rspec/example_group.rb +63 -31
- data/lib/datadog/ci/contrib/simplecov/ext.rb +2 -0
- data/lib/datadog/ci/contrib/simplecov/patcher.rb +2 -0
- data/lib/datadog/ci/contrib/simplecov/report_uploader.rb +59 -0
- data/lib/datadog/ci/ext/environment/providers/github_actions.rb +65 -2
- data/lib/datadog/ci/ext/environment.rb +10 -0
- data/lib/datadog/ci/ext/settings.rb +3 -0
- data/lib/datadog/ci/ext/telemetry.rb +5 -0
- data/lib/datadog/ci/ext/test.rb +0 -5
- data/lib/datadog/ci/ext/transport.rb +4 -0
- data/lib/datadog/ci/git/cli.rb +59 -1
- data/lib/datadog/ci/remote/component.rb +6 -1
- data/lib/datadog/ci/remote/library_settings.rb +8 -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.rb +27 -18
- data/lib/datadog/ci/test_management/component.rb +9 -0
- data/lib/datadog/ci/test_management/null_component.rb +8 -0
- data/lib/datadog/ci/test_optimisation/component.rb +202 -20
- data/lib/datadog/ci/test_optimisation/null_component.rb +19 -5
- data/lib/datadog/ci/test_suite.rb +16 -21
- data/lib/datadog/ci/test_visibility/component.rb +1 -2
- data/lib/datadog/ci/test_visibility/known_tests.rb +59 -6
- data/lib/datadog/ci/transport/api/agentless.rb +8 -1
- data/lib/datadog/ci/transport/api/base.rb +21 -0
- data/lib/datadog/ci/transport/api/builder.rb +5 -1
- data/lib/datadog/ci/transport/api/evp_proxy.rb +8 -0
- data/lib/datadog/ci/version.rb +1 -1
- metadata +19 -4
- 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,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "../ext/telemetry"
|
|
6
|
+
require_relative "../ext/transport"
|
|
7
|
+
require_relative "../transport/gzip"
|
|
8
|
+
require_relative "../transport/telemetry"
|
|
9
|
+
require_relative "../utils/telemetry"
|
|
10
|
+
|
|
11
|
+
module Datadog
|
|
12
|
+
module CI
|
|
13
|
+
module CodeCoverage
|
|
14
|
+
class Transport
|
|
15
|
+
attr_reader :api
|
|
16
|
+
|
|
17
|
+
def initialize(api:)
|
|
18
|
+
@api = api
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def send_coverage_report(event:, coverage_report:)
|
|
22
|
+
return nil if api.nil?
|
|
23
|
+
|
|
24
|
+
Datadog.logger.debug { "[#{self.class.name}] Sending coverage report..." }
|
|
25
|
+
|
|
26
|
+
compressed_coverage_report = CI::Transport::Gzip.compress(coverage_report)
|
|
27
|
+
event_json = event.to_json
|
|
28
|
+
|
|
29
|
+
response = api.cicovreprt_request(
|
|
30
|
+
path: Ext::Transport::CODE_COVERAGE_REPORT_INTAKE_PATH,
|
|
31
|
+
event_payload: event_json,
|
|
32
|
+
compressed_coverage_report: compressed_coverage_report
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
CI::Transport::Telemetry.api_requests(
|
|
36
|
+
Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST,
|
|
37
|
+
1,
|
|
38
|
+
compressed: response.request_compressed
|
|
39
|
+
)
|
|
40
|
+
Utils::Telemetry.distribution(
|
|
41
|
+
Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST_MS,
|
|
42
|
+
response.duration_ms
|
|
43
|
+
)
|
|
44
|
+
Utils::Telemetry.distribution(
|
|
45
|
+
Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST_BYTES,
|
|
46
|
+
compressed_coverage_report.bytesize.to_f,
|
|
47
|
+
{Ext::Telemetry::TAG_REQUEST_COMPRESSED => response.request_compressed.to_s}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
unless response.ok?
|
|
51
|
+
CI::Transport::Telemetry.api_requests_errors(
|
|
52
|
+
Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST_ERRORS,
|
|
53
|
+
1,
|
|
54
|
+
error_type: response.telemetry_error_type,
|
|
55
|
+
status_code: response.code
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
Datadog.logger.warn { "[#{self.class.name}] Failed to send coverage report: #{response.inspect}" }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
response
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "datadog/core/telemetry/ext"
|
|
4
4
|
|
|
5
5
|
require_relative "../ext/settings"
|
|
6
|
+
require_relative "../code_coverage/component"
|
|
7
|
+
require_relative "../code_coverage/null_component"
|
|
8
|
+
require_relative "../code_coverage/transport"
|
|
6
9
|
require_relative "../git/tree_uploader"
|
|
7
10
|
require_relative "../impacted_tests_detection/component"
|
|
8
11
|
require_relative "../impacted_tests_detection/null_component"
|
|
@@ -42,7 +45,7 @@ module Datadog
|
|
|
42
45
|
# Adds CI behavior to Datadog trace components
|
|
43
46
|
module Components
|
|
44
47
|
attr_reader :test_visibility, :test_optimisation, :git_tree_upload_worker, :ci_remote, :test_retries,
|
|
45
|
-
:test_management, :agentless_logs_submission, :impacted_tests_detection, :test_discovery
|
|
48
|
+
:test_management, :agentless_logs_submission, :impacted_tests_detection, :test_discovery, :code_coverage
|
|
46
49
|
|
|
47
50
|
def initialize(settings)
|
|
48
51
|
@test_optimisation = TestOptimisation::NullComponent.new
|
|
@@ -53,6 +56,7 @@ module Datadog
|
|
|
53
56
|
@test_management = TestManagement::NullComponent.new
|
|
54
57
|
@impacted_tests_detection = ImpactedTestsDetection::NullComponent.new
|
|
55
58
|
@test_discovery = TestDiscovery::NullComponent.new
|
|
59
|
+
@code_coverage = CodeCoverage::NullComponent.new
|
|
56
60
|
|
|
57
61
|
# Activate CI mode if enabled
|
|
58
62
|
if settings.ci.enabled
|
|
@@ -69,6 +73,7 @@ module Datadog
|
|
|
69
73
|
@test_optimisation&.shutdown!
|
|
70
74
|
@agentless_logs_submission&.shutdown!
|
|
71
75
|
@test_discovery&.shutdown!
|
|
76
|
+
@code_coverage&.shutdown!
|
|
72
77
|
@git_tree_upload_worker&.stop
|
|
73
78
|
end
|
|
74
79
|
|
|
@@ -158,6 +163,8 @@ module Datadog
|
|
|
158
163
|
@agentless_logs_submission = build_agentless_logs_component(settings, test_visibility_api)
|
|
159
164
|
|
|
160
165
|
@impacted_tests_detection = ImpactedTestsDetection::Component.new(enabled: settings.ci.impacted_tests_detection_enabled)
|
|
166
|
+
|
|
167
|
+
@code_coverage = build_code_coverage(settings, test_visibility_api)
|
|
161
168
|
end
|
|
162
169
|
|
|
163
170
|
def build_test_optimisation(settings, test_visibility_api)
|
|
@@ -192,7 +199,8 @@ module Datadog
|
|
|
192
199
|
enabled: settings.ci.enabled && settings.ci.itr_enabled,
|
|
193
200
|
bundle_location: settings.ci.itr_code_coverage_excluded_bundle_path,
|
|
194
201
|
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
|
|
202
|
+
use_allocation_tracing: settings.ci.itr_test_impact_analysis_use_allocation_tracing,
|
|
203
|
+
static_dependencies_tracking_enabled: settings.ci.tia_static_dependencies_tracking_enabled
|
|
196
204
|
)
|
|
197
205
|
end
|
|
198
206
|
|
|
@@ -282,6 +290,15 @@ module Datadog
|
|
|
282
290
|
)
|
|
283
291
|
end
|
|
284
292
|
|
|
293
|
+
def build_code_coverage(settings, api)
|
|
294
|
+
return CodeCoverage::NullComponent.new if api.nil? || settings.ci.discard_traces
|
|
295
|
+
|
|
296
|
+
CodeCoverage::Component.new(
|
|
297
|
+
enabled: settings.ci.code_coverage_report_upload_enabled,
|
|
298
|
+
transport: CodeCoverage::Transport.new(api: api)
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
285
302
|
def build_agentless_logs_component(settings, api)
|
|
286
303
|
if settings.ci.agentless_logs_submission_enabled && !settings.ci.agentless_mode_enabled
|
|
287
304
|
Datadog.logger.warn(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../contrib/instrumentation"
|
|
4
4
|
require_relative "../ext/settings"
|
|
5
5
|
require_relative "../utils/bundle"
|
|
6
|
+
require_relative "../utils/parsing"
|
|
6
7
|
|
|
7
8
|
module Datadog
|
|
8
9
|
module CI
|
|
@@ -19,8 +20,19 @@ module Datadog
|
|
|
19
20
|
settings :ci do
|
|
20
21
|
option :enabled do |o|
|
|
21
22
|
o.type :bool
|
|
22
|
-
o.env CI::Ext::Settings::
|
|
23
|
-
o.default
|
|
23
|
+
o.env CI::Ext::Settings::ENV_ENABLED
|
|
24
|
+
o.default do
|
|
25
|
+
env_value = ENV[CI::Ext::Settings::ENV_MODE_ENABLED]
|
|
26
|
+
if env_value && !ENV[CI::Ext::Settings::ENV_ENABLED]
|
|
27
|
+
Datadog::Core.log_deprecation do
|
|
28
|
+
"#{CI::Ext::Settings::ENV_MODE_ENABLED} environment variable is deprecated, " \
|
|
29
|
+
"use #{CI::Ext::Settings::ENV_ENABLED} instead."
|
|
30
|
+
end
|
|
31
|
+
Utils::Parsing.convert_to_bool(env_value)
|
|
32
|
+
else
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
option :test_session_name do |o|
|
|
@@ -168,6 +180,18 @@ module Datadog
|
|
|
168
180
|
o.env CI::Ext::Settings::ENV_TEST_DISCOVERY_OUTPUT_PATH
|
|
169
181
|
end
|
|
170
182
|
|
|
183
|
+
option :tia_static_dependencies_tracking_enabled do |o|
|
|
184
|
+
o.type :bool
|
|
185
|
+
o.env CI::Ext::Settings::ENV_TIA_STATIC_DEPENDENCIES_TRACKING_ENABLED
|
|
186
|
+
o.default false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
option :code_coverage_report_upload_enabled do |o|
|
|
190
|
+
o.type :bool
|
|
191
|
+
o.env CI::Ext::Settings::ENV_CODE_COVERAGE_REPORT_UPLOAD_ENABLED
|
|
192
|
+
o.default true
|
|
193
|
+
end
|
|
194
|
+
|
|
171
195
|
define_method(:instrument) do |integration_name, options = {}, &block|
|
|
172
196
|
return unless enabled
|
|
173
197
|
|
|
@@ -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
|
|
@@ -64,7 +64,7 @@ module Datadog
|
|
|
64
64
|
|
|
65
65
|
finish_with_result(test_span, result_code)
|
|
66
66
|
|
|
67
|
-
# remove failures if
|
|
67
|
+
# remove failures if failure can be ignored because of retries
|
|
68
68
|
self.failures = [] if test_span.should_ignore_failures?
|
|
69
69
|
|
|
70
70
|
super
|
|
@@ -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,9 +36,12 @@ 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
|
+
# we keep track of the last test failure if we encounter any
|
|
43
|
+
test_failure = nil
|
|
44
|
+
|
|
42
45
|
test_retries_component.with_retries do
|
|
43
46
|
test_visibility_component.trace_test(
|
|
44
47
|
datadog_test_name,
|
|
@@ -46,6 +49,9 @@ module Datadog
|
|
|
46
49
|
tags: tags,
|
|
47
50
|
service: datadog_configuration[:service_name]
|
|
48
51
|
) do |test_span|
|
|
52
|
+
# Set context IDs on the test span for TIA context coverage merging
|
|
53
|
+
test_span&.context_ids = datadog_context_ids
|
|
54
|
+
|
|
49
55
|
test_span&.itr_unskippable! if datadog_unskippable?
|
|
50
56
|
|
|
51
57
|
metadata[:skip] = test_span&.datadog_skip_reason if test_span&.should_skip?
|
|
@@ -65,8 +71,10 @@ module Datadog
|
|
|
65
71
|
test_span&.passed!
|
|
66
72
|
when :failed
|
|
67
73
|
test_span&.failed!(exception: execution_result.exception)
|
|
74
|
+
|
|
68
75
|
# if any of the retries passed or test is quarantined, we don't fail the test run
|
|
69
76
|
@exception = nil if test_span&.should_ignore_failures?
|
|
77
|
+
test_failure = @exception
|
|
70
78
|
else
|
|
71
79
|
# :pending or nil
|
|
72
80
|
test_span&.skipped!(
|
|
@@ -92,6 +100,12 @@ module Datadog
|
|
|
92
100
|
metadata[Ext::METADATA_DD_SKIPPED_BY_ITR] = true
|
|
93
101
|
end
|
|
94
102
|
end
|
|
103
|
+
|
|
104
|
+
# at this point if we have encountered any test failure in any of the previous retries
|
|
105
|
+
# we restore the @exception internal state if we should not skip failures for this run
|
|
106
|
+
if test_failure && !test_span&.should_ignore_failures?
|
|
107
|
+
@exception = test_failure
|
|
108
|
+
end
|
|
95
109
|
end
|
|
96
110
|
end
|
|
97
111
|
|
|
@@ -172,19 +186,45 @@ module Datadog
|
|
|
172
186
|
Git::LocalRepository.relative_to_root(metadata[:rerun_file_path])
|
|
173
187
|
end
|
|
174
188
|
|
|
189
|
+
# Returns list of context IDs for this example, from outermost to innermost.
|
|
190
|
+
# Used for merging context-level coverage into test coverage.
|
|
191
|
+
def datadog_context_ids
|
|
192
|
+
traverse_example_group_hierarchy unless defined?(@datadog_context_ids)
|
|
193
|
+
@datadog_context_ids
|
|
194
|
+
end
|
|
195
|
+
|
|
175
196
|
private
|
|
176
197
|
|
|
177
|
-
def
|
|
198
|
+
def datadog_top_level_example_group
|
|
199
|
+
traverse_example_group_hierarchy unless defined?(@datadog_top_level_example_group)
|
|
200
|
+
@datadog_top_level_example_group
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Traverses the example group hierarchy once, populating both
|
|
204
|
+
# @datadog_context_ids and @top_level_example_group.
|
|
205
|
+
def traverse_example_group_hierarchy
|
|
206
|
+
context_ids = []
|
|
178
207
|
example_group = metadata[:example_group]
|
|
179
|
-
|
|
208
|
+
top_level = example_group
|
|
209
|
+
|
|
210
|
+
# Walk up the example group hierarchy
|
|
211
|
+
while example_group
|
|
212
|
+
# Use scoped_id as the stable identifier, fallback to file:line
|
|
213
|
+
context_id = example_group[:scoped_id] ||
|
|
214
|
+
"#{example_group[:file_path]}:#{example_group[:line_number]}"
|
|
215
|
+
|
|
216
|
+
context_ids << context_id
|
|
217
|
+
top_level = example_group
|
|
218
|
+
|
|
219
|
+
example_group = example_group[:parent_example_group]
|
|
220
|
+
end
|
|
180
221
|
|
|
181
|
-
|
|
222
|
+
@datadog_context_ids = context_ids
|
|
223
|
+
@datadog_top_level_example_group = top_level
|
|
182
224
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
res = parent
|
|
225
|
+
Datadog.logger.debug do
|
|
226
|
+
"RSpec: Built context chain for the test: #{context_ids.inspect}"
|
|
186
227
|
end
|
|
187
|
-
res
|
|
188
228
|
end
|
|
189
229
|
|
|
190
230
|
def datadog_integration
|
|
@@ -196,7 +236,7 @@ module Datadog
|
|
|
196
236
|
end
|
|
197
237
|
|
|
198
238
|
def datadog_test_suite_description
|
|
199
|
-
@datadog_test_suite_description ||=
|
|
239
|
+
@datadog_test_suite_description ||= datadog_top_level_example_group[:description]
|
|
200
240
|
end
|
|
201
241
|
|
|
202
242
|
def test_visibility_component
|
|
@@ -24,47 +24,79 @@ module Datadog
|
|
|
24
24
|
# even if it is a nested context
|
|
25
25
|
metadata[:skip] = true if all_examples_skipped_by_datadog?
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
# Start context coverage for this example group (for TIA suite-level coverage).
|
|
28
|
+
# This captures code executed in before(:context)/before(:all) hooks.
|
|
29
|
+
# The context_id uses scoped_id which is a stable identifier for RSpec example groups.
|
|
30
|
+
context_id = datadog_context_id
|
|
31
|
+
start_context_coverage(context_id)
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
# return early because we create Datadog::CI::TestSuite only for top-level example groups
|
|
35
|
+
return super unless top_level?
|
|
36
|
+
|
|
37
|
+
suite_name = "#{description} at #{file_path}"
|
|
38
|
+
suite_tags = {
|
|
39
|
+
CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(metadata[:file_path]),
|
|
40
|
+
CI::Ext::Test::TAG_SOURCE_START => metadata[:line_number].to_s
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test_suite =
|
|
44
|
+
test_visibility_component&.start_test_suite(
|
|
45
|
+
suite_name,
|
|
46
|
+
tags: suite_tags,
|
|
47
|
+
service: datadog_configuration[:service_name]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
success = super
|
|
51
|
+
|
|
52
|
+
return success unless test_suite
|
|
53
|
+
|
|
54
|
+
test_suite.finish
|
|
55
|
+
|
|
56
|
+
success
|
|
57
|
+
ensure
|
|
58
|
+
clear_context_coverage(context_id)
|
|
52
59
|
end
|
|
53
|
-
|
|
54
|
-
test_suite.finish
|
|
55
|
-
|
|
56
|
-
success
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
private
|
|
60
63
|
|
|
61
64
|
def all_examples_skipped_by_datadog?
|
|
62
65
|
descendant_filtered_examples.all? do |example|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
datadog_test_id = example.datadog_test_id
|
|
67
|
+
datadog_fqn_test_id = example.datadog_fqn_test_id
|
|
68
|
+
|
|
69
|
+
# Don't skip if the test is marked as attempt_to_fix (it should run and be retried)
|
|
70
|
+
next false if test_management_component&.attempt_to_fix?(datadog_fqn_test_id)
|
|
71
|
+
|
|
72
|
+
# Skip by Test Impact Analysis: test is skippable and not marked as unskippable
|
|
73
|
+
tia_skipped = !example.datadog_unskippable? &&
|
|
74
|
+
test_optimisation_component&.skippable?(datadog_test_id)
|
|
75
|
+
|
|
76
|
+
# Skip by Test Management: test is disabled
|
|
77
|
+
test_management_disabled = test_management_component&.disabled?(datadog_fqn_test_id)
|
|
78
|
+
|
|
79
|
+
tia_skipped || test_management_disabled
|
|
65
80
|
end
|
|
66
81
|
end
|
|
67
82
|
|
|
83
|
+
# Returns a stable context ID for this example group.
|
|
84
|
+
# Uses RSpec's scoped_id which uniquely identifies each example group.
|
|
85
|
+
def datadog_context_id
|
|
86
|
+
metadata[:scoped_id] || "#{metadata[:file_path]}:#{metadata[:line_number]}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Starts context coverage collection for this example group.
|
|
90
|
+
# This captures code executed in before(:context)/before(:all) hooks.
|
|
91
|
+
def start_context_coverage(context_id)
|
|
92
|
+
test_optimisation_component&.on_test_context_started(context_id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clears context coverage for this example group after it finishes.
|
|
96
|
+
def clear_context_coverage(context_id)
|
|
97
|
+
test_optimisation_component&.clear_context_coverage(context_id)
|
|
98
|
+
end
|
|
99
|
+
|
|
68
100
|
def datadog_configuration
|
|
69
101
|
Datadog.configuration.ci[:rspec]
|
|
70
102
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../patcher"
|
|
4
4
|
|
|
5
|
+
require_relative "report_uploader"
|
|
5
6
|
require_relative "result_extractor"
|
|
6
7
|
|
|
7
8
|
module Datadog
|
|
@@ -16,6 +17,7 @@ module Datadog
|
|
|
16
17
|
|
|
17
18
|
def patch
|
|
18
19
|
::SimpleCov.include(ResultExtractor)
|
|
20
|
+
::SimpleCov.include(ReportUploader)
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ext"
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module CI
|
|
7
|
+
module Contrib
|
|
8
|
+
module Simplecov
|
|
9
|
+
# Module that hooks into SimpleCov.process_results_and_report_error to upload coverage reports
|
|
10
|
+
module ReportUploader
|
|
11
|
+
def self.included(base)
|
|
12
|
+
base.singleton_class.prepend(ClassMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
def process_results_and_report_error(*args)
|
|
17
|
+
result = super
|
|
18
|
+
|
|
19
|
+
upload_coverage_report
|
|
20
|
+
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def upload_coverage_report
|
|
27
|
+
return unless datadog_configuration[:enabled]
|
|
28
|
+
|
|
29
|
+
code_coverage = ::Datadog.send(:components).code_coverage
|
|
30
|
+
return unless code_coverage.enabled
|
|
31
|
+
|
|
32
|
+
serialized_report = read_coverage_result
|
|
33
|
+
return if serialized_report.nil?
|
|
34
|
+
|
|
35
|
+
code_coverage.upload(serialized_report: serialized_report, format: Ext::COVERAGE_FORMAT)
|
|
36
|
+
rescue => e
|
|
37
|
+
Datadog.logger.warn("Failed to upload coverage report: #{e.message}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read_coverage_result
|
|
41
|
+
coverage_file = File.join(::SimpleCov.coverage_path, ".resultset.json")
|
|
42
|
+
|
|
43
|
+
unless File.exist?(coverage_file)
|
|
44
|
+
Datadog.logger.debug { "Coverage result file not found at #{coverage_file}" }
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
File.read(coverage_file)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def datadog_configuration
|
|
52
|
+
Datadog.configuration.ci[:simplecov]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -15,6 +15,13 @@ module Datadog
|
|
|
15
15
|
# Github Actions: https://github.com/features/actions
|
|
16
16
|
# Environment variables docs: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
17
17
|
class GithubActions < Base
|
|
18
|
+
# Paths to GitHub Actions runner diagnostics folder
|
|
19
|
+
# GitHub-hosted (SaaS) runners use the cached path, self-hosted runners use the non-cached path
|
|
20
|
+
GITHUB_RUNNER_DIAG_PATHS = [
|
|
21
|
+
"/home/runner/actions-runner/cached/_diag", # GitHub-hosted (SaaS) runners
|
|
22
|
+
"/home/runner/actions-runner/_diag" # Self-hosted runners
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
18
25
|
def self.handles?(env)
|
|
19
26
|
env.key?("GITHUB_SHA")
|
|
20
27
|
end
|
|
@@ -28,11 +35,16 @@ module Datadog
|
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def job_url
|
|
31
|
-
|
|
38
|
+
numeric_id = numeric_job_id
|
|
39
|
+
if numeric_id
|
|
40
|
+
"#{github_server_url}/#{env["GITHUB_REPOSITORY"]}/actions/runs/#{env["GITHUB_RUN_ID"]}/job/#{numeric_id}"
|
|
41
|
+
else
|
|
42
|
+
"#{github_server_url}/#{env["GITHUB_REPOSITORY"]}/commit/#{env["GITHUB_SHA"]}/checks"
|
|
43
|
+
end
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
def job_id
|
|
35
|
-
env["GITHUB_JOB"]
|
|
47
|
+
numeric_job_id || env["GITHUB_JOB"]
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def pipeline_id
|
|
@@ -141,6 +153,57 @@ module Datadog
|
|
|
141
153
|
|
|
142
154
|
@github_server_url ||= Datadog::Core::Utils::Url.filter_basic_auth(env["GITHUB_SERVER_URL"])
|
|
143
155
|
end
|
|
156
|
+
|
|
157
|
+
# Returns numeric job ID from environment variable or runner diagnostics.
|
|
158
|
+
# Priority:
|
|
159
|
+
# 1. JOB_CHECK_RUN_ID environment variable (GitHub Actions feature pending)
|
|
160
|
+
# 2. Worker_*.log files in the runner's _diag folder (fallback)
|
|
161
|
+
def numeric_job_id
|
|
162
|
+
return @numeric_job_id if defined?(@numeric_job_id)
|
|
163
|
+
|
|
164
|
+
@numeric_job_id = env["JOB_CHECK_RUN_ID"] || extract_numeric_job_id_from_diag_files
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_numeric_job_id_from_diag_files
|
|
168
|
+
GITHUB_RUNNER_DIAG_PATHS.each do |diag_path|
|
|
169
|
+
next unless Dir.exist?(diag_path)
|
|
170
|
+
|
|
171
|
+
worker_files = Dir.glob(File.join(diag_path, "Worker_*.log")).sort
|
|
172
|
+
next if worker_files.empty?
|
|
173
|
+
|
|
174
|
+
# Use the most recent worker file (last in sorted order)
|
|
175
|
+
worker_file = worker_files.last
|
|
176
|
+
check_run_id = extract_check_run_id_from_worker_file(worker_file)
|
|
177
|
+
return check_run_id if check_run_id
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
nil
|
|
181
|
+
rescue => e
|
|
182
|
+
Datadog.logger.debug("Failed to extract numeric job ID from GitHub Actions runner diagnostics: #{e}")
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Regex to extract check_run_id value from Worker log files.
|
|
187
|
+
# The log format varies between GitHub-hosted and self-hosted runners,
|
|
188
|
+
# so we use regex instead of JSON parsing for robustness.
|
|
189
|
+
# Matches patterns like: "k": "check_run_id" ... "v": 12345 or "v": 12345.0
|
|
190
|
+
CHECK_RUN_ID_REGEX = /"k":\s*"check_run_id"[^}]*"v":\s*(\d+)(?:\.\d+)?/
|
|
191
|
+
|
|
192
|
+
def extract_check_run_id_from_worker_file(file_path)
|
|
193
|
+
return nil unless File.exist?(file_path)
|
|
194
|
+
|
|
195
|
+
content = File.read(file_path)
|
|
196
|
+
|
|
197
|
+
# Find all check_run_id values in the file.
|
|
198
|
+
# On self-hosted runners, Worker_*.log files can be appended across multiple jobs,
|
|
199
|
+
# so we use the last match to get the current job's ID.
|
|
200
|
+
# flatten because scan with capture groups returns Array[Array[String]]
|
|
201
|
+
matches = content.scan(CHECK_RUN_ID_REGEX).flatten
|
|
202
|
+
matches.last
|
|
203
|
+
rescue => e
|
|
204
|
+
Datadog.logger.debug("Failed to parse Worker log file #{file_path}: #{e}")
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
144
207
|
end
|
|
145
208
|
end
|
|
146
209
|
end
|
|
@@ -53,6 +53,14 @@ module Datadog
|
|
|
53
53
|
module_function
|
|
54
54
|
|
|
55
55
|
def tags(env)
|
|
56
|
+
@tags ||= extract_tags(env).freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reset!
|
|
60
|
+
@tags = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_tags(env)
|
|
56
64
|
# Extract metadata from CI provider environment variables
|
|
57
65
|
tags = Environment::Extractor.new(env).tags
|
|
58
66
|
|
|
@@ -91,6 +99,8 @@ module Datadog
|
|
|
91
99
|
tags
|
|
92
100
|
end
|
|
93
101
|
|
|
102
|
+
private_class_method :extract_tags
|
|
103
|
+
|
|
94
104
|
def ensure_post_conditions(tags)
|
|
95
105
|
validate_repository_url(tags[Git::TAG_REPOSITORY_URL])
|
|
96
106
|
validate_git_sha(tags[Git::TAG_COMMIT_SHA])
|