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
|
@@ -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
|
|
@@ -65,6 +69,16 @@ module Datadog
|
|
|
65
69
|
|
|
66
70
|
@mutex = Mutex.new
|
|
67
71
|
|
|
72
|
+
# Context coverage: stores coverage collected during before(:context)/before(:all) hooks
|
|
73
|
+
# keyed by context_id (e.g., RSpec scoped_id for example groups)
|
|
74
|
+
# Only used when use_single_threaded_coverage is false (multi-threaded mode)
|
|
75
|
+
@context_coverages = {}
|
|
76
|
+
@context_coverages_mutex = Mutex.new
|
|
77
|
+
|
|
78
|
+
# Currently active context ID for context coverage collection
|
|
79
|
+
@current_context_id = nil
|
|
80
|
+
@current_context_id_mutex = Mutex.new
|
|
81
|
+
|
|
68
82
|
Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
|
|
69
83
|
end
|
|
70
84
|
|
|
@@ -82,7 +96,11 @@ module Datadog
|
|
|
82
96
|
# we skip tests, not suites
|
|
83
97
|
test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, Ext::Test::ITR_TEST_SKIPPING_MODE)
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
if @code_coverage_enabled
|
|
100
|
+
load_datadog_cov!
|
|
101
|
+
|
|
102
|
+
populate_static_dependencies_map!
|
|
103
|
+
end
|
|
86
104
|
|
|
87
105
|
# Load component state first, and if successful, skip fetching skippable tests
|
|
88
106
|
# Also try to restore from DDTest cache if available
|
|
@@ -109,47 +127,154 @@ module Datadog
|
|
|
109
127
|
@code_coverage_enabled
|
|
110
128
|
end
|
|
111
129
|
|
|
112
|
-
|
|
130
|
+
# Starts coverage collection.
|
|
131
|
+
# This is a low-level method that only starts the collector.
|
|
132
|
+
#
|
|
133
|
+
# @return [void]
|
|
134
|
+
def start_coverage
|
|
113
135
|
return if !enabled? || !code_coverage?
|
|
114
136
|
|
|
115
|
-
Telemetry.code_coverage_started(test)
|
|
116
137
|
coverage_collector&.start
|
|
117
138
|
end
|
|
118
139
|
|
|
119
|
-
|
|
140
|
+
# Stops coverage collection and returns raw coverage data.
|
|
141
|
+
# This is a low-level method that only stops the collector.
|
|
142
|
+
#
|
|
143
|
+
# @return [Hash, nil] Raw coverage data or nil
|
|
144
|
+
def stop_coverage
|
|
145
|
+
return if !enabled? || !code_coverage?
|
|
146
|
+
|
|
147
|
+
coverage_collector&.stop
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Called when a test context (e.g., RSpec example group with before(:context)) starts.
|
|
151
|
+
# Starts collecting coverage that will be merged into all tests within this context.
|
|
152
|
+
#
|
|
153
|
+
# @param context_id [String] A stable identifier for the context (e.g., RSpec scoped_id)
|
|
154
|
+
# @return [void]
|
|
155
|
+
def on_test_context_started(context_id)
|
|
156
|
+
return unless context_coverage_enabled?
|
|
157
|
+
|
|
158
|
+
# Stop and store any existing context coverage before starting new one.
|
|
159
|
+
# This ensures that outer context coverage is preserved when nested contexts start.
|
|
160
|
+
stop_context_coverage_and_store
|
|
161
|
+
|
|
162
|
+
Datadog.logger.debug { "Starting context coverage collection for context [#{context_id}]" }
|
|
163
|
+
|
|
164
|
+
# Store the context_id we're collecting for
|
|
165
|
+
@current_context_id_mutex.synchronize do
|
|
166
|
+
@current_context_id = context_id
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
coverage_collector&.start
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Called when a test starts within a context. This method:
|
|
173
|
+
# 1. Stops any in-progress context coverage collection and stores it
|
|
174
|
+
# 2. Starts coverage collection for the test itself
|
|
175
|
+
#
|
|
176
|
+
# @param test [Datadog::CI::Test] The test that is starting
|
|
177
|
+
# @return [void]
|
|
178
|
+
def on_test_started(test)
|
|
120
179
|
return if !enabled? || !code_coverage?
|
|
121
180
|
|
|
181
|
+
# Stop any in-progress context coverage and store it
|
|
182
|
+
stop_context_coverage_and_store
|
|
183
|
+
|
|
184
|
+
Telemetry.code_coverage_started(test)
|
|
185
|
+
|
|
186
|
+
context_ids = test.context_ids || []
|
|
187
|
+
|
|
188
|
+
Datadog.logger.debug do
|
|
189
|
+
"Starting test coverage for [#{test.name}] with context chain: #{context_ids.inspect}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
coverage_collector&.start
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Called when a test finishes. This method:
|
|
196
|
+
# 1. Stops test coverage collection
|
|
197
|
+
# 2. Merges context coverage from all relevant contexts
|
|
198
|
+
# 3. Writes the combined coverage event
|
|
199
|
+
# 4. Records ITR statistics if test was skipped by TIA
|
|
200
|
+
#
|
|
201
|
+
# @param test [Datadog::CI::Test] The test that finished
|
|
202
|
+
# @param context [Datadog::CI::TestVisibility::Context] The test visibility context for ITR stats
|
|
203
|
+
# @return [Datadog::CI::TestOptimisation::Coverage::Event, nil] The coverage event or nil
|
|
204
|
+
def on_test_finished(test, context)
|
|
205
|
+
return unless enabled?
|
|
206
|
+
|
|
207
|
+
# Handle ITR statistics
|
|
208
|
+
if test.skipped_by_test_impact_analysis?
|
|
209
|
+
Telemetry.itr_skipped
|
|
210
|
+
|
|
211
|
+
context.incr_tests_skipped_by_tia_count
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Handle code coverage
|
|
215
|
+
return unless code_coverage?
|
|
122
216
|
Telemetry.code_coverage_finished(test)
|
|
123
217
|
|
|
124
218
|
coverage = coverage_collector&.stop
|
|
125
219
|
|
|
126
220
|
# if test was skipped, we discard coverage data
|
|
127
221
|
return if test.skipped?
|
|
222
|
+
coverage ||= {}
|
|
128
223
|
|
|
129
|
-
|
|
224
|
+
# Merge context coverage from all relevant contexts
|
|
225
|
+
context_ids = test.context_ids || []
|
|
226
|
+
merge_context_coverages_into_test(coverage, context_ids)
|
|
227
|
+
|
|
228
|
+
if coverage.empty?
|
|
130
229
|
Telemetry.code_coverage_is_empty
|
|
131
230
|
return
|
|
132
231
|
end
|
|
133
232
|
|
|
233
|
+
# cucumber's gherkin files are not covered by the code coverage collector - we add them here explicitly
|
|
134
234
|
test_source_file = test.source_file
|
|
135
|
-
|
|
136
|
-
# cucumber's gherkin files are not covered by the code coverage collector
|
|
137
235
|
ensure_test_source_covered(test_source_file, coverage) unless test_source_file.nil?
|
|
138
236
|
|
|
237
|
+
# if we have static dependencies tracking enabled then we can make the coverage
|
|
238
|
+
# more precise by fetching which files we depend on based on constants usage
|
|
239
|
+
enrich_coverage_with_static_dependencies(coverage)
|
|
240
|
+
|
|
139
241
|
Telemetry.code_coverage_files(coverage.size)
|
|
140
242
|
|
|
141
|
-
|
|
243
|
+
coverage_event = Coverage::Event.new(
|
|
142
244
|
test_id: test.id.to_s,
|
|
143
245
|
test_suite_id: test.test_suite_id.to_s,
|
|
144
246
|
test_session_id: test.test_session_id.to_s,
|
|
145
247
|
coverage: coverage
|
|
146
248
|
)
|
|
147
249
|
|
|
148
|
-
Datadog.logger.debug { "Writing coverage event \n #{
|
|
250
|
+
Datadog.logger.debug { "Writing coverage event \n #{coverage_event.pretty_inspect}" }
|
|
251
|
+
|
|
252
|
+
write(coverage_event)
|
|
253
|
+
|
|
254
|
+
coverage_event
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Clears stored context coverage for a specific context.
|
|
258
|
+
# Should be called when a context finishes (e.g., after(:context) completes).
|
|
259
|
+
#
|
|
260
|
+
# @param context_id [String] The context ID to clear
|
|
261
|
+
# @return [void]
|
|
262
|
+
def clear_context_coverage(context_id)
|
|
263
|
+
return unless context_coverage_enabled?
|
|
149
264
|
|
|
150
|
-
|
|
265
|
+
@context_coverages_mutex.synchronize do
|
|
266
|
+
@context_coverages.delete(context_id)
|
|
151
267
|
|
|
152
|
-
|
|
268
|
+
Datadog.logger.debug { "Cleared context coverage for [#{context_id}]" }
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Returns whether context coverage collection is enabled.
|
|
273
|
+
# Context coverage is disabled in single-threaded mode.
|
|
274
|
+
#
|
|
275
|
+
# @return [Boolean]
|
|
276
|
+
def context_coverage_enabled?
|
|
277
|
+
enabled? && code_coverage? && !@use_single_threaded_coverage
|
|
153
278
|
end
|
|
154
279
|
|
|
155
280
|
def skippable?(datadog_test_id)
|
|
@@ -170,14 +295,6 @@ module Datadog
|
|
|
170
295
|
end
|
|
171
296
|
end
|
|
172
297
|
|
|
173
|
-
def on_test_finished(test, context)
|
|
174
|
-
return if !test.skipped? || !test.skipped_by_test_impact_analysis?
|
|
175
|
-
|
|
176
|
-
Telemetry.itr_skipped
|
|
177
|
-
|
|
178
|
-
context.incr_tests_skipped_by_tia_count
|
|
179
|
-
end
|
|
180
|
-
|
|
181
298
|
def write_test_session_tags(test_session, skipped_tests_count)
|
|
182
299
|
return if !enabled?
|
|
183
300
|
|
|
@@ -323,6 +440,25 @@ module Datadog
|
|
|
323
440
|
@code_coverage_enabled = false
|
|
324
441
|
end
|
|
325
442
|
|
|
443
|
+
def populate_static_dependencies_map!
|
|
444
|
+
return unless @code_coverage_enabled
|
|
445
|
+
return unless @static_dependencies_tracking_enabled
|
|
446
|
+
|
|
447
|
+
Datadog::CI::SourceCode::StaticDependencies.populate!(Git::LocalRepository.root, @bundle_location)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def enrich_coverage_with_static_dependencies(coverage)
|
|
451
|
+
return unless @static_dependencies_tracking_enabled
|
|
452
|
+
|
|
453
|
+
static_dependencies_map = {}
|
|
454
|
+
coverage.keys.each do |file|
|
|
455
|
+
static_dependencies_map.merge!(
|
|
456
|
+
Datadog::CI::SourceCode::StaticDependencies.fetch_static_dependencies(file)
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
coverage.merge!(static_dependencies_map)
|
|
460
|
+
end
|
|
461
|
+
|
|
326
462
|
def ensure_test_source_covered(test_source_file, coverage)
|
|
327
463
|
absolute_test_source_file_path = File.join(Git::LocalRepository.root, test_source_file)
|
|
328
464
|
return if coverage.key?(absolute_test_source_file_path)
|
|
@@ -360,6 +496,52 @@ module Datadog
|
|
|
360
496
|
def git_tree_upload_worker
|
|
361
497
|
Datadog.send(:components).git_tree_upload_worker
|
|
362
498
|
end
|
|
499
|
+
|
|
500
|
+
# Stops any in-progress context coverage collection and stores it.
|
|
501
|
+
# Called when a test starts to capture coverage from before(:context) hooks.
|
|
502
|
+
def stop_context_coverage_and_store
|
|
503
|
+
return unless context_coverage_enabled?
|
|
504
|
+
|
|
505
|
+
context_id = @current_context_id_mutex.synchronize do
|
|
506
|
+
id = @current_context_id
|
|
507
|
+
@current_context_id = nil
|
|
508
|
+
id
|
|
509
|
+
end
|
|
510
|
+
return if context_id.nil?
|
|
511
|
+
|
|
512
|
+
coverage = coverage_collector&.stop
|
|
513
|
+
return if coverage.nil? || coverage.empty?
|
|
514
|
+
|
|
515
|
+
@context_coverages_mutex.synchronize do
|
|
516
|
+
@context_coverages[context_id] = coverage
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
Datadog.logger.debug do
|
|
520
|
+
"Stored context coverage for [#{context_id}] with #{coverage.size} files"
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Merges context coverage from all relevant contexts into the test's coverage.
|
|
525
|
+
#
|
|
526
|
+
# @param coverage [Hash] The test's coverage hash to merge into
|
|
527
|
+
# @param context_ids [Array<String>] List of context IDs to merge coverage from
|
|
528
|
+
def merge_context_coverages_into_test(coverage, context_ids)
|
|
529
|
+
return if @use_single_threaded_coverage
|
|
530
|
+
return if context_ids.empty?
|
|
531
|
+
|
|
532
|
+
@context_coverages_mutex.synchronize do
|
|
533
|
+
context_ids.each do |context_id|
|
|
534
|
+
context_coverage = @context_coverages[context_id]
|
|
535
|
+
next unless context_coverage
|
|
536
|
+
|
|
537
|
+
coverage.merge!(context_coverage)
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
Datadog.logger.debug do
|
|
542
|
+
"Merged context coverage for contexts: #{context_ids.inspect} into test coverage"
|
|
543
|
+
end
|
|
544
|
+
end
|
|
363
545
|
end
|
|
364
546
|
end
|
|
365
547
|
end
|
|
@@ -34,21 +34,35 @@ module Datadog
|
|
|
34
34
|
false
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def start_coverage
|
|
37
|
+
def start_coverage
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def stop_coverage
|
|
40
|
+
def stop_coverage
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def on_test_context_started(_context_id)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
false
|
|
47
|
+
def on_test_started(_test)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
def on_test_finished(_test, _context)
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clear_context_coverage(_context_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def context_coverage_enabled?
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def mark_if_skippable(_test)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def skippable?(_datadog_test_id)
|
|
65
|
+
false
|
|
52
66
|
end
|
|
53
67
|
|
|
54
68
|
def write_test_session_tags(_test_session, _skipped_tests_count)
|
|
@@ -19,6 +19,10 @@ module Datadog
|
|
|
19
19
|
# counts how many times every test in this suite was executed with each status:
|
|
20
20
|
# { "MySuite.mytest.a:1" => { "pass" => 3, "fail" => 2 } }
|
|
21
21
|
@execution_stats_per_test = {}
|
|
22
|
+
|
|
23
|
+
# tracks final status for each test (the status that is reported after all retries):
|
|
24
|
+
# { "MySuite.mytest.a:1" => "pass" }
|
|
25
|
+
@final_statuses_per_test = {}
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
# Finishes this test suite.
|
|
@@ -42,6 +46,13 @@ module Datadog
|
|
|
42
46
|
end
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
# @internal
|
|
50
|
+
def record_test_final_status(test_id, final_status)
|
|
51
|
+
synchronize do
|
|
52
|
+
@final_statuses_per_test[test_id] = final_status
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
# @internal
|
|
46
57
|
def any_passed?
|
|
47
58
|
synchronize do
|
|
@@ -63,8 +74,7 @@ module Datadog
|
|
|
63
74
|
def all_executions_failed?(test_id)
|
|
64
75
|
synchronize do
|
|
65
76
|
stats = @execution_stats_per_test[test_id]
|
|
66
|
-
stats &&
|
|
67
|
-
stats[Ext::Test::Status::PASS] == 0
|
|
77
|
+
stats && stats[Ext::Test::Status::FAIL] > 0 && stats[Ext::Test::Status::PASS] == 0
|
|
68
78
|
end
|
|
69
79
|
end
|
|
70
80
|
|
|
@@ -72,8 +82,7 @@ module Datadog
|
|
|
72
82
|
def all_executions_passed?(test_id)
|
|
73
83
|
synchronize do
|
|
74
84
|
stats = @execution_stats_per_test[test_id]
|
|
75
|
-
stats && stats[Ext::Test::Status::PASS] > 0 && stats[Ext::Test::Status::FAIL] == 0
|
|
76
|
-
stats[Ext::Test::ExecutionStatsStatus::FAIL_IGNORED] == 0
|
|
85
|
+
stats && stats[Ext::Test::Status::PASS] > 0 && stats[Ext::Test::Status::FAIL] == 0
|
|
77
86
|
end
|
|
78
87
|
end
|
|
79
88
|
|
|
@@ -106,9 +115,9 @@ module Datadog
|
|
|
106
115
|
|
|
107
116
|
def set_status_from_stats!
|
|
108
117
|
synchronize do
|
|
109
|
-
# count how many tests
|
|
110
|
-
test_suite_stats = @
|
|
111
|
-
acc[
|
|
118
|
+
# count how many tests have each final status
|
|
119
|
+
test_suite_stats = @final_statuses_per_test.each_with_object(Hash.new(0)) do |(_test_id, final_status), acc|
|
|
120
|
+
acc[final_status] += 1
|
|
112
121
|
end
|
|
113
122
|
|
|
114
123
|
# test suite is considered failed if at least one test failed
|
|
@@ -123,20 +132,6 @@ module Datadog
|
|
|
123
132
|
end
|
|
124
133
|
end
|
|
125
134
|
end
|
|
126
|
-
|
|
127
|
-
def derive_test_status_from_execution_stats(test_execution_stats)
|
|
128
|
-
# test is passed if it passed at least once or it failed but fail was ignored
|
|
129
|
-
if test_execution_stats[Ext::Test::Status::PASS] > 0 ||
|
|
130
|
-
test_execution_stats[Ext::Test::ExecutionStatsStatus::FAIL_IGNORED] > 0
|
|
131
|
-
Ext::Test::Status::PASS
|
|
132
|
-
# if test was never passed, it is failed if it failed at least once
|
|
133
|
-
elsif test_execution_stats[Ext::Test::Status::FAIL] > 0
|
|
134
|
-
Ext::Test::Status::FAIL
|
|
135
|
-
# otherwise it is skipped
|
|
136
|
-
else
|
|
137
|
-
Ext::Test::Status::SKIP
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
135
|
end
|
|
141
136
|
end
|
|
142
137
|
end
|
|
@@ -302,7 +302,7 @@ module Datadog
|
|
|
302
302
|
test_management.tag_test_from_properties(test)
|
|
303
303
|
|
|
304
304
|
test_optimisation.mark_if_skippable(test)
|
|
305
|
-
test_optimisation.
|
|
305
|
+
test_optimisation.on_test_started(test)
|
|
306
306
|
|
|
307
307
|
test_retries.record_test_started(test)
|
|
308
308
|
end
|
|
@@ -328,7 +328,6 @@ module Datadog
|
|
|
328
328
|
end
|
|
329
329
|
|
|
330
330
|
def on_test_finished(test)
|
|
331
|
-
test_optimisation.stop_coverage(test)
|
|
332
331
|
test_optimisation.on_test_finished(test, maybe_remote_context)
|
|
333
332
|
|
|
334
333
|
validate_source_location(test)
|
|
@@ -45,6 +45,14 @@ module Datadog
|
|
|
45
45
|
res
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def cursor
|
|
49
|
+
page_info.fetch("cursor", nil)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def has_next?
|
|
53
|
+
page_info.fetch("has_next", false)
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
private
|
|
49
57
|
|
|
50
58
|
def initialize(http_response, json)
|
|
@@ -66,6 +74,13 @@ module Datadog
|
|
|
66
74
|
@json = {}
|
|
67
75
|
end
|
|
68
76
|
end
|
|
77
|
+
|
|
78
|
+
def page_info
|
|
79
|
+
payload
|
|
80
|
+
.fetch("data", {})
|
|
81
|
+
.fetch("attributes", {})
|
|
82
|
+
.fetch("page_info", {})
|
|
83
|
+
end
|
|
69
84
|
end
|
|
70
85
|
|
|
71
86
|
def initialize(dd_env:, api: nil, config_tags: {})
|
|
@@ -78,8 +93,45 @@ module Datadog
|
|
|
78
93
|
api = @api
|
|
79
94
|
return Set.new unless api
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
result = Set.new
|
|
97
|
+
page_state = nil
|
|
98
|
+
page_number = 1
|
|
99
|
+
|
|
100
|
+
loop do
|
|
101
|
+
Datadog.logger.debug { "Fetching known tests page ##{page_number}#{" with cursor" if page_state}" }
|
|
102
|
+
|
|
103
|
+
response = fetch_page(api, test_session, page_state: page_state)
|
|
104
|
+
|
|
105
|
+
unless response.ok?
|
|
106
|
+
Datadog.logger.debug(
|
|
107
|
+
"Failed to fetch known tests page ##{page_number}, bailing out of known tests fetch. " \
|
|
108
|
+
"Early flake detection will not work."
|
|
109
|
+
)
|
|
110
|
+
return Set.new
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
page_tests = response.tests
|
|
114
|
+
result.merge(page_tests)
|
|
115
|
+
Datadog.logger.debug { "Received #{page_tests.size} known tests from page ##{page_number} (total so far: #{result.size})" }
|
|
116
|
+
|
|
117
|
+
unless response.has_next?
|
|
118
|
+
Datadog.logger.debug { "Stopping known tests fetch: no more pages after page ##{page_number}" }
|
|
119
|
+
break
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
page_state = response.cursor
|
|
123
|
+
page_number += 1
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Datadog.logger.debug { "Finished fetching known tests: #{result.size} tests total from #{page_number} page(s)" }
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def fetch_page(api, test_session, page_state: nil)
|
|
133
|
+
request_payload = payload(test_session, page_state: page_state)
|
|
134
|
+
Datadog.logger.debug { "Known tests request payload: #{request_payload}" }
|
|
83
135
|
|
|
84
136
|
http_response = api.api_request(
|
|
85
137
|
path: Ext::Transport::DD_API_UNIQUE_TESTS_PATH,
|
|
@@ -107,12 +159,12 @@ module Datadog
|
|
|
107
159
|
)
|
|
108
160
|
end
|
|
109
161
|
|
|
110
|
-
Response.from_http_response(http_response)
|
|
162
|
+
Response.from_http_response(http_response)
|
|
111
163
|
end
|
|
112
164
|
|
|
113
|
-
|
|
165
|
+
def payload(test_session, page_state: nil)
|
|
166
|
+
page_info = page_state ? {"page_state" => page_state} : {}
|
|
114
167
|
|
|
115
|
-
def payload(test_session)
|
|
116
168
|
{
|
|
117
169
|
"data" => {
|
|
118
170
|
"id" => Datadog::Core::Environment::Identity.id,
|
|
@@ -129,7 +181,8 @@ module Datadog
|
|
|
129
181
|
Ext::Test::TAG_RUNTIME_NAME => test_session.runtime_name,
|
|
130
182
|
Ext::Test::TAG_RUNTIME_VERSION => test_session.runtime_version,
|
|
131
183
|
"custom" => @config_tags
|
|
132
|
-
}
|
|
184
|
+
},
|
|
185
|
+
"page_info" => page_info
|
|
133
186
|
}
|
|
134
187
|
}
|
|
135
188
|
}.to_json
|
|
@@ -10,12 +10,13 @@ module Datadog
|
|
|
10
10
|
class Agentless < Base
|
|
11
11
|
attr_reader :api_key
|
|
12
12
|
|
|
13
|
-
def initialize(api_key:, citestcycle_url:, api_url:, citestcov_url:, logs_intake_url:)
|
|
13
|
+
def initialize(api_key:, citestcycle_url:, api_url:, citestcov_url:, logs_intake_url:, cicovreprt_url:)
|
|
14
14
|
@api_key = api_key
|
|
15
15
|
@citestcycle_http = build_http_client(citestcycle_url, compress: true)
|
|
16
16
|
@api_http = build_http_client(api_url, compress: false)
|
|
17
17
|
@citestcov_http = build_http_client(citestcov_url, compress: true)
|
|
18
18
|
@logs_intake_http = build_http_client(logs_intake_url, compress: true)
|
|
19
|
+
@cicovreprt_http = build_http_client(cicovreprt_url, compress: false)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def citestcycle_request(path:, payload:, headers: {}, verb: "post")
|
|
@@ -49,6 +50,12 @@ module Datadog
|
|
|
49
50
|
perform_request(@logs_intake_http, path: path, payload: payload, headers: headers, verb: verb)
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
def cicovreprt_request(path:, event_payload:, compressed_coverage_report:, headers: {}, verb: "post")
|
|
54
|
+
super
|
|
55
|
+
|
|
56
|
+
perform_request(@cicovreprt_http, path: path, payload: @cicovreprt_payload, headers: headers, verb: verb)
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
private
|
|
53
60
|
|
|
54
61
|
def perform_request(http_client, path:, payload:, headers:, verb:, accept_compressed_response: false)
|
|
@@ -42,6 +42,27 @@ module Datadog
|
|
|
42
42
|
headers[Ext::Transport::HEADER_CONTENT_TYPE] ||= Ext::Transport::CONTENT_TYPE_JSON
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def cicovreprt_request(path:, event_payload:, compressed_coverage_report:, headers: {}, verb: "post")
|
|
46
|
+
cicovreprt_request_boundary = ::SecureRandom.uuid
|
|
47
|
+
|
|
48
|
+
headers[Ext::Transport::HEADER_CONTENT_TYPE] ||=
|
|
49
|
+
"#{Ext::Transport::CONTENT_TYPE_MULTIPART_FORM_DATA}; boundary=#{cicovreprt_request_boundary}"
|
|
50
|
+
|
|
51
|
+
@cicovreprt_payload = [
|
|
52
|
+
"--#{cicovreprt_request_boundary}",
|
|
53
|
+
'Content-Disposition: form-data; name="event"; filename="event.json"',
|
|
54
|
+
"Content-Type: application/json",
|
|
55
|
+
"",
|
|
56
|
+
event_payload,
|
|
57
|
+
"--#{cicovreprt_request_boundary}",
|
|
58
|
+
'Content-Disposition: form-data; name="coverage"; filename="coverage.gz"',
|
|
59
|
+
"Content-Type: application/octet-stream",
|
|
60
|
+
"",
|
|
61
|
+
compressed_coverage_report,
|
|
62
|
+
"--#{cicovreprt_request_boundary}--"
|
|
63
|
+
].join("\r\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
def headers_with_default(headers)
|
|
46
67
|
request_headers = default_headers
|
|
47
68
|
request_headers.merge!(headers)
|
|
@@ -30,12 +30,16 @@ module Datadog
|
|
|
30
30
|
logs_intake_url = settings.ci.agentless_url ||
|
|
31
31
|
"https://#{Ext::Transport::LOGS_INTAKE_HOST_PREFIX}.#{dd_site}:443"
|
|
32
32
|
|
|
33
|
+
cicovreprt_url = settings.ci.agentless_url ||
|
|
34
|
+
"https://#{Ext::Transport::CODE_COVERAGE_REPORT_INTAKE_HOST_PREFIX}.#{dd_site}:443"
|
|
35
|
+
|
|
33
36
|
Agentless.new(
|
|
34
37
|
api_key: settings.api_key,
|
|
35
38
|
citestcycle_url: citestcycle_url,
|
|
36
39
|
api_url: api_url,
|
|
37
40
|
citestcov_url: citestcov_url,
|
|
38
|
-
logs_intake_url: logs_intake_url
|
|
41
|
+
logs_intake_url: logs_intake_url,
|
|
42
|
+
cicovreprt_url: cicovreprt_url
|
|
39
43
|
)
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -50,6 +50,14 @@ module Datadog
|
|
|
50
50
|
raise NotImplementedError, "Logs intake is not supported in EVP proxy mode"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def cicovreprt_request(path:, event_payload:, compressed_coverage_report:, headers: {}, verb: "post")
|
|
54
|
+
super
|
|
55
|
+
|
|
56
|
+
headers[Ext::Transport::HEADER_EVP_SUBDOMAIN] = Ext::Transport::CODE_COVERAGE_REPORT_INTAKE_HOST_PREFIX
|
|
57
|
+
|
|
58
|
+
perform_request(@agent_intake_http, path: path, payload: @cicovreprt_payload, headers: headers, verb: verb)
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
private
|
|
54
62
|
|
|
55
63
|
def perform_request(http_client, path:, payload:, headers:, verb:)
|