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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -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/code_coverage/component.rb +55 -0
  15. data/lib/datadog/ci/code_coverage/null_component.rb +24 -0
  16. data/lib/datadog/ci/code_coverage/transport.rb +66 -0
  17. data/lib/datadog/ci/configuration/components.rb +19 -2
  18. data/lib/datadog/ci/configuration/settings.rb +26 -2
  19. data/lib/datadog/ci/contrib/minitest/helpers.rb +3 -3
  20. data/lib/datadog/ci/contrib/minitest/parallel_executor_minitest_6.rb +0 -7
  21. data/lib/datadog/ci/contrib/minitest/test.rb +3 -3
  22. data/lib/datadog/ci/contrib/rspec/example.rb +50 -10
  23. data/lib/datadog/ci/contrib/rspec/example_group.rb +63 -31
  24. data/lib/datadog/ci/contrib/simplecov/ext.rb +2 -0
  25. data/lib/datadog/ci/contrib/simplecov/patcher.rb +2 -0
  26. data/lib/datadog/ci/contrib/simplecov/report_uploader.rb +59 -0
  27. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +65 -2
  28. data/lib/datadog/ci/ext/environment.rb +10 -0
  29. data/lib/datadog/ci/ext/settings.rb +3 -0
  30. data/lib/datadog/ci/ext/telemetry.rb +5 -0
  31. data/lib/datadog/ci/ext/test.rb +0 -5
  32. data/lib/datadog/ci/ext/transport.rb +4 -0
  33. data/lib/datadog/ci/git/cli.rb +59 -1
  34. data/lib/datadog/ci/remote/component.rb +6 -1
  35. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  36. data/lib/datadog/ci/source_code/constant_resolver.rb +43 -0
  37. data/lib/datadog/ci/{utils/source_code.rb → source_code/method_inspect.rb} +3 -3
  38. data/lib/datadog/ci/source_code/path_filter.rb +33 -0
  39. data/lib/datadog/ci/source_code/static_dependencies.rb +71 -0
  40. data/lib/datadog/ci/source_code/static_dependencies_extractor.rb +237 -0
  41. data/lib/datadog/ci/test.rb +27 -18
  42. data/lib/datadog/ci/test_management/component.rb +9 -0
  43. data/lib/datadog/ci/test_management/null_component.rb +8 -0
  44. data/lib/datadog/ci/test_optimisation/component.rb +202 -20
  45. data/lib/datadog/ci/test_optimisation/null_component.rb +19 -5
  46. data/lib/datadog/ci/test_suite.rb +16 -21
  47. data/lib/datadog/ci/test_visibility/component.rb +1 -2
  48. data/lib/datadog/ci/test_visibility/known_tests.rb +59 -6
  49. data/lib/datadog/ci/transport/api/agentless.rb +8 -1
  50. data/lib/datadog/ci/transport/api/base.rb +21 -0
  51. data/lib/datadog/ci/transport/api/builder.rb +5 -1
  52. data/lib/datadog/ci/transport/api/evp_proxy.rb +8 -0
  53. data/lib/datadog/ci/version.rb +1 -1
  54. metadata +19 -4
  55. data/ext/datadog_ci_native/datadog_source_code.c +0 -28
  56. data/ext/datadog_ci_native/datadog_source_code.h +0 -3
@@ -19,6 +19,14 @@ module Datadog
19
19
 
20
20
  def tag_test_from_properties(_)
21
21
  end
22
+
23
+ def attempt_to_fix?(_datadog_fqn_test_id)
24
+ false
25
+ end
26
+
27
+ def disabled?(_datadog_fqn_test_id)
28
+ false
29
+ end
22
30
  end
23
31
  end
24
32
  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
@@ -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
- load_datadog_cov! if @code_coverage_enabled
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
- def start_coverage(test)
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
- def stop_coverage(test)
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
- if coverage.nil? || coverage.empty?
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
- event = Coverage::Event.new(
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 #{event.pretty_inspect}" }
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
- write(event)
265
+ @context_coverages_mutex.synchronize do
266
+ @context_coverages.delete(context_id)
151
267
 
152
- event
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(_test)
37
+ def start_coverage
38
38
  end
39
39
 
40
- def stop_coverage(_test)
40
+ def stop_coverage
41
41
  nil
42
42
  end
43
43
 
44
- def mark_if_skippable(_test)
44
+ def on_test_context_started(_context_id)
45
45
  end
46
46
 
47
- def skippable?(_datadog_test_id)
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 && (stats[Ext::Test::Status::FAIL] > 0 || stats[Ext::Test::ExecutionStatsStatus::FAIL_IGNORED] > 0) &&
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 passed, failed and skipped
110
- test_suite_stats = @execution_stats_per_test.each_with_object(Hash.new(0)) do |(_test_id, stats), acc|
111
- acc[derive_test_status_from_execution_stats(stats)] += 1
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.start_coverage(test)
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
- request_payload = payload(test_session)
82
- Datadog.logger.debug("Fetching unique known tests with request: #{request_payload}")
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).tests
162
+ Response.from_http_response(http_response)
111
163
  end
112
164
 
113
- private
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:)
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 25
7
+ MINOR = 27
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil