datadog-ci 1.26.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/lib/datadog/ci/code_coverage/component.rb +55 -0
  4. data/lib/datadog/ci/code_coverage/null_component.rb +24 -0
  5. data/lib/datadog/ci/code_coverage/transport.rb +66 -0
  6. data/lib/datadog/ci/configuration/components.rb +17 -1
  7. data/lib/datadog/ci/configuration/settings.rb +20 -2
  8. data/lib/datadog/ci/contrib/minitest/test.rb +1 -1
  9. data/lib/datadog/ci/contrib/rspec/example.rb +48 -8
  10. data/lib/datadog/ci/contrib/rspec/example_group.rb +63 -31
  11. data/lib/datadog/ci/contrib/simplecov/ext.rb +2 -0
  12. data/lib/datadog/ci/contrib/simplecov/patcher.rb +2 -0
  13. data/lib/datadog/ci/contrib/simplecov/report_uploader.rb +59 -0
  14. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +65 -2
  15. data/lib/datadog/ci/ext/environment.rb +10 -0
  16. data/lib/datadog/ci/ext/settings.rb +2 -0
  17. data/lib/datadog/ci/ext/telemetry.rb +5 -0
  18. data/lib/datadog/ci/ext/test.rb +0 -5
  19. data/lib/datadog/ci/ext/transport.rb +4 -0
  20. data/lib/datadog/ci/git/cli.rb +59 -1
  21. data/lib/datadog/ci/remote/component.rb +6 -1
  22. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  23. data/lib/datadog/ci/test.rb +27 -18
  24. data/lib/datadog/ci/test_management/component.rb +9 -0
  25. data/lib/datadog/ci/test_management/null_component.rb +8 -0
  26. data/lib/datadog/ci/test_optimisation/component.rb +168 -16
  27. data/lib/datadog/ci/test_optimisation/null_component.rb +19 -5
  28. data/lib/datadog/ci/test_suite.rb +16 -21
  29. data/lib/datadog/ci/test_visibility/component.rb +1 -2
  30. data/lib/datadog/ci/test_visibility/known_tests.rb +59 -6
  31. data/lib/datadog/ci/transport/api/agentless.rb +8 -1
  32. data/lib/datadog/ci/transport/api/base.rb +21 -0
  33. data/lib/datadog/ci/transport/api/builder.rb +5 -1
  34. data/lib/datadog/ci/transport/api/evp_proxy.rb +8 -0
  35. data/lib/datadog/ci/version.rb +1 -1
  36. metadata +5 -1
@@ -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 = 26
7
+ MINOR = 27
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.0
4
+ version: 1.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
@@ -92,6 +92,9 @@ files:
92
92
  - lib/datadog/ci/cli/command/exec.rb
93
93
  - lib/datadog/ci/cli/command/skippable_tests_percentage.rb
94
94
  - lib/datadog/ci/cli/command/skippable_tests_percentage_estimate.rb
95
+ - lib/datadog/ci/code_coverage/component.rb
96
+ - lib/datadog/ci/code_coverage/null_component.rb
97
+ - lib/datadog/ci/code_coverage/transport.rb
95
98
  - lib/datadog/ci/codeowners/matcher.rb
96
99
  - lib/datadog/ci/codeowners/parser.rb
97
100
  - lib/datadog/ci/codeowners/rule.rb
@@ -172,6 +175,7 @@ files:
172
175
  - lib/datadog/ci/contrib/simplecov/ext.rb
173
176
  - lib/datadog/ci/contrib/simplecov/integration.rb
174
177
  - lib/datadog/ci/contrib/simplecov/patcher.rb
178
+ - lib/datadog/ci/contrib/simplecov/report_uploader.rb
175
179
  - lib/datadog/ci/contrib/simplecov/result_extractor.rb
176
180
  - lib/datadog/ci/ext/app_types.rb
177
181
  - lib/datadog/ci/ext/dd_test.rb