datadog-ci 1.3.0 → 1.4.1
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/README.md +1 -0
- data/ext/datadog_cov/datadog_cov.c +1 -1
- data/lib/datadog/ci/configuration/components.rb +25 -11
- data/lib/datadog/ci/configuration/settings.rb +19 -1
- data/lib/datadog/ci/contrib/cucumber/configuration_override.rb +37 -0
- data/lib/datadog/ci/contrib/cucumber/formatter.rb +5 -5
- data/lib/datadog/ci/contrib/cucumber/instrumentation.rb +9 -1
- data/lib/datadog/ci/contrib/cucumber/patcher.rb +3 -3
- data/lib/datadog/ci/contrib/minitest/runner.rb +16 -0
- data/lib/datadog/ci/contrib/minitest/test.rb +1 -0
- data/lib/datadog/ci/contrib/rspec/example.rb +67 -39
- data/lib/datadog/ci/contrib/rspec/example_group.rb +1 -1
- data/lib/datadog/ci/ext/settings.rb +3 -0
- data/lib/datadog/ci/ext/telemetry.rb +1 -0
- data/lib/datadog/ci/ext/test.rb +11 -11
- data/lib/datadog/ci/ext/transport.rb +1 -0
- data/lib/datadog/ci/git/local_repository.rb +32 -13
- data/lib/datadog/ci/remote/component.rb +50 -0
- data/lib/datadog/ci/remote/library_settings.rb +91 -0
- data/lib/datadog/ci/{transport/remote_settings_api.rb → remote/library_settings_client.rb} +11 -56
- data/lib/datadog/ci/test.rb +8 -1
- data/lib/datadog/ci/test_optimisation/component.rb +12 -16
- data/lib/datadog/ci/test_retries/component.rb +84 -0
- data/lib/datadog/ci/test_retries/null_component.rb +28 -0
- data/lib/datadog/ci/test_retries/strategy/base.rb +19 -0
- data/lib/datadog/ci/test_retries/strategy/no_retry.rb +16 -0
- data/lib/datadog/ci/test_retries/strategy/retry_failed.rb +37 -0
- data/lib/datadog/ci/test_suite.rb +39 -18
- data/lib/datadog/ci/test_visibility/component.rb +45 -47
- data/lib/datadog/ci/test_visibility/null_component.rb +6 -0
- data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
- data/lib/datadog/ci/version.rb +2 -2
- metadata +13 -6
- data/lib/datadog/ci/contrib/cucumber/step.rb +0 -27
@@ -238,23 +238,42 @@ module Datadog
|
|
238
238
|
Telemetry.git_command(Ext::Telemetry::Command::UNSHALLOW)
|
239
239
|
res = nil
|
240
240
|
|
241
|
+
unshallow_command =
|
242
|
+
"git fetch " \
|
243
|
+
"--shallow-since=\"1 month ago\" " \
|
244
|
+
"--update-shallow " \
|
245
|
+
"--filter=\"blob:none\" " \
|
246
|
+
"--recurse-submodules=no " \
|
247
|
+
"$(git config --default origin --get clone.defaultRemoteName)"
|
248
|
+
|
249
|
+
unshallow_remotes = [
|
250
|
+
"$(git rev-parse HEAD)",
|
251
|
+
"$(git rev-parse --abbrev-ref --symbolic-full-name @{upstream})",
|
252
|
+
nil
|
253
|
+
]
|
254
|
+
|
241
255
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
256
|
+
unshallow_remotes.each do |remote|
|
257
|
+
unshallowing_errored = false
|
258
|
+
|
259
|
+
res =
|
260
|
+
begin
|
261
|
+
exec_git_command(
|
262
|
+
"#{unshallow_command} #{remote}"
|
263
|
+
)
|
264
|
+
rescue => e
|
265
|
+
log_failure(e, "git unshallow")
|
266
|
+
telemetry_track_error(e, Ext::Telemetry::Command::UNSHALLOW)
|
267
|
+
unshallowing_errored = true
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
|
271
|
+
break unless unshallowing_errored
|
272
|
+
end
|
250
273
|
end
|
251
|
-
Telemetry.git_command_ms(Ext::Telemetry::Command::UNSHALLOW, duration_ms)
|
252
274
|
|
275
|
+
Telemetry.git_command_ms(Ext::Telemetry::Command::UNSHALLOW, duration_ms)
|
253
276
|
res
|
254
|
-
rescue => e
|
255
|
-
log_failure(e, "git unshallow")
|
256
|
-
telemetry_track_error(e, Ext::Telemetry::Command::UNSHALLOW)
|
257
|
-
nil
|
258
277
|
end
|
259
278
|
|
260
279
|
# makes .exec_git_command private to make sure that this method
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module CI
|
5
|
+
module Remote
|
6
|
+
# Remote configuration component.
|
7
|
+
# Responsible for fetching library settings and configuring the library accordingly.
|
8
|
+
class Component
|
9
|
+
def initialize(library_settings_client:)
|
10
|
+
@library_settings_client = library_settings_client
|
11
|
+
end
|
12
|
+
|
13
|
+
# called on test session start, uses test session info to send configuration request to the backend
|
14
|
+
def configure(test_session)
|
15
|
+
library_configuration = @library_settings_client.fetch(test_session)
|
16
|
+
# sometimes we can skip code coverage for default branch if there are no changes in the repository
|
17
|
+
# backend needs git metadata uploaded for this test session to check if we can skip code coverage
|
18
|
+
if library_configuration.require_git?
|
19
|
+
Datadog.logger.debug { "Library configuration endpoint requires git upload to be finished, waiting..." }
|
20
|
+
git_tree_upload_worker.wait_until_done
|
21
|
+
|
22
|
+
Datadog.logger.debug { "Requesting library configuration again..." }
|
23
|
+
library_configuration = @library_settings_client.fetch(test_session)
|
24
|
+
|
25
|
+
if library_configuration.require_git?
|
26
|
+
Datadog.logger.debug { "git metadata upload did not complete in time when configuring library" }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test_optimisation.configure(library_configuration, test_session)
|
31
|
+
test_retries.configure(library_configuration)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def test_optimisation
|
37
|
+
Datadog.send(:components).test_optimisation
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_retries
|
41
|
+
Datadog.send(:components).test_retries
|
42
|
+
end
|
43
|
+
|
44
|
+
def git_tree_upload_worker
|
45
|
+
Datadog.send(:components).git_tree_upload_worker
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
require_relative "../ext/telemetry"
|
6
|
+
require_relative "../ext/transport"
|
7
|
+
require_relative "../transport/telemetry"
|
8
|
+
require_relative "../utils/parsing"
|
9
|
+
|
10
|
+
module Datadog
|
11
|
+
module CI
|
12
|
+
module Remote
|
13
|
+
# Wrapper around the settings HTTP response
|
14
|
+
class LibrarySettings
|
15
|
+
def initialize(http_response)
|
16
|
+
@http_response = http_response
|
17
|
+
@json = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def ok?
|
21
|
+
resp = @http_response
|
22
|
+
!resp.nil? && resp.ok?
|
23
|
+
end
|
24
|
+
|
25
|
+
def payload
|
26
|
+
cached = @json
|
27
|
+
return cached unless cached.nil?
|
28
|
+
|
29
|
+
resp = @http_response
|
30
|
+
return @json = default_payload if resp.nil? || !ok?
|
31
|
+
|
32
|
+
begin
|
33
|
+
@json = JSON.parse(resp.payload).dig(*Ext::Transport::DD_API_SETTINGS_RESPONSE_DIG_KEYS) ||
|
34
|
+
default_payload
|
35
|
+
rescue JSON::ParserError => e
|
36
|
+
Datadog.logger.error("Failed to parse settings response payload: #{e}. Payload was: #{resp.payload}")
|
37
|
+
|
38
|
+
Transport::Telemetry.api_requests_errors(
|
39
|
+
Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_ERRORS,
|
40
|
+
1,
|
41
|
+
error_type: "invalid_json",
|
42
|
+
status_code: nil
|
43
|
+
)
|
44
|
+
|
45
|
+
@json = default_payload
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def require_git?
|
50
|
+
return @require_git if defined?(@require_git)
|
51
|
+
|
52
|
+
@require_git = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY)
|
53
|
+
end
|
54
|
+
|
55
|
+
def itr_enabled?
|
56
|
+
return @itr_enabled if defined?(@itr_enabled)
|
57
|
+
|
58
|
+
@itr_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY)
|
59
|
+
end
|
60
|
+
|
61
|
+
def code_coverage_enabled?
|
62
|
+
return @code_coverage_enabled if defined?(@code_coverage_enabled)
|
63
|
+
|
64
|
+
@code_coverage_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY)
|
65
|
+
end
|
66
|
+
|
67
|
+
def tests_skipping_enabled?
|
68
|
+
return @tests_skipping_enabled if defined?(@tests_skipping_enabled)
|
69
|
+
|
70
|
+
@tests_skipping_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY)
|
71
|
+
end
|
72
|
+
|
73
|
+
def flaky_test_retries_enabled?
|
74
|
+
return @flaky_test_retries_enabled if defined?(@flaky_test_retries_enabled)
|
75
|
+
|
76
|
+
@flaky_test_retries_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def bool(key)
|
82
|
+
Utils::Parsing.convert_to_bool(payload.fetch(key, false))
|
83
|
+
end
|
84
|
+
|
85
|
+
def default_payload
|
86
|
+
Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -4,73 +4,28 @@ require "json"
|
|
4
4
|
|
5
5
|
require "datadog/core/environment/identity"
|
6
6
|
|
7
|
+
require_relative "library_settings"
|
8
|
+
|
9
|
+
require_relative "../ext/test"
|
7
10
|
require_relative "../ext/telemetry"
|
8
11
|
require_relative "../ext/transport"
|
9
12
|
require_relative "../transport/telemetry"
|
10
|
-
require_relative "../utils/parsing"
|
11
13
|
require_relative "../utils/telemetry"
|
12
14
|
|
13
15
|
module Datadog
|
14
16
|
module CI
|
15
|
-
module
|
16
|
-
# Datadog API client
|
17
|
+
module Remote
|
17
18
|
# Calls settings endpoint to fetch library settings for given service and env
|
18
|
-
class
|
19
|
-
class Response
|
20
|
-
def initialize(http_response)
|
21
|
-
@http_response = http_response
|
22
|
-
@json = nil
|
23
|
-
end
|
24
|
-
|
25
|
-
def ok?
|
26
|
-
resp = @http_response
|
27
|
-
!resp.nil? && resp.ok?
|
28
|
-
end
|
29
|
-
|
30
|
-
def payload
|
31
|
-
cached = @json
|
32
|
-
return cached unless cached.nil?
|
33
|
-
|
34
|
-
resp = @http_response
|
35
|
-
return @json = default_payload if resp.nil? || !ok?
|
36
|
-
|
37
|
-
begin
|
38
|
-
@json = JSON.parse(resp.payload).dig(*Ext::Transport::DD_API_SETTINGS_RESPONSE_DIG_KEYS) ||
|
39
|
-
default_payload
|
40
|
-
rescue JSON::ParserError => e
|
41
|
-
Datadog.logger.error("Failed to parse settings response payload: #{e}. Payload was: #{resp.payload}")
|
42
|
-
|
43
|
-
Transport::Telemetry.api_requests_errors(
|
44
|
-
Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_ERRORS,
|
45
|
-
1,
|
46
|
-
error_type: "invalid_json",
|
47
|
-
status_code: nil
|
48
|
-
)
|
49
|
-
|
50
|
-
@json = default_payload
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def require_git?
|
55
|
-
Utils::Parsing.convert_to_bool(payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY])
|
56
|
-
end
|
57
|
-
|
58
|
-
private
|
59
|
-
|
60
|
-
def default_payload
|
61
|
-
Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
19
|
+
class LibrarySettingsClient
|
65
20
|
def initialize(dd_env:, api: nil, config_tags: {})
|
66
21
|
@api = api
|
67
22
|
@dd_env = dd_env
|
68
23
|
@config_tags = config_tags || {}
|
69
24
|
end
|
70
25
|
|
71
|
-
def
|
26
|
+
def fetch(test_session)
|
72
27
|
api = @api
|
73
|
-
return
|
28
|
+
return LibrarySettings.new(nil) unless api
|
74
29
|
|
75
30
|
request_payload = payload(test_session)
|
76
31
|
Datadog.logger.debug("Fetching library settings with request: #{request_payload}")
|
@@ -96,18 +51,18 @@ module Datadog
|
|
96
51
|
)
|
97
52
|
end
|
98
53
|
|
99
|
-
|
54
|
+
library_settings = LibrarySettings.new(http_response)
|
100
55
|
|
101
56
|
Utils::Telemetry.inc(
|
102
57
|
Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_RESPONSE,
|
103
58
|
1,
|
104
59
|
{
|
105
|
-
Ext::Telemetry::TAG_COVERAGE_ENABLED =>
|
106
|
-
Ext::Telemetry::TAG_ITR_SKIP_ENABLED =>
|
60
|
+
Ext::Telemetry::TAG_COVERAGE_ENABLED => library_settings.code_coverage_enabled?.to_s,
|
61
|
+
Ext::Telemetry::TAG_ITR_SKIP_ENABLED => library_settings.tests_skipping_enabled?.to_s
|
107
62
|
}
|
108
63
|
)
|
109
64
|
|
110
|
-
|
65
|
+
library_settings
|
111
66
|
end
|
112
67
|
|
113
68
|
private
|
data/lib/datadog/ci/test.rb
CHANGED
@@ -142,7 +142,14 @@ module Datadog
|
|
142
142
|
private
|
143
143
|
|
144
144
|
def record_test_result(datadog_status)
|
145
|
-
|
145
|
+
test_id = Utils::TestRun.skippable_test_id(name, test_suite_name, parameters)
|
146
|
+
|
147
|
+
# if this test was already executed in this test suite, mark it as retried
|
148
|
+
if test_suite&.test_executed?(test_id)
|
149
|
+
set_tag(Ext::Test::TAG_IS_RETRY, "true")
|
150
|
+
end
|
151
|
+
|
152
|
+
test_suite&.record_test_result(test_id, datadog_status)
|
146
153
|
end
|
147
154
|
end
|
148
155
|
end
|
@@ -6,7 +6,6 @@ require "datadog/core/utils/forking"
|
|
6
6
|
|
7
7
|
require_relative "../ext/test"
|
8
8
|
require_relative "../ext/telemetry"
|
9
|
-
require_relative "../ext/transport"
|
10
9
|
|
11
10
|
require_relative "../git/local_repository"
|
12
11
|
|
@@ -65,24 +64,17 @@ module Datadog
|
|
65
64
|
Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
|
66
65
|
end
|
67
66
|
|
68
|
-
def configure(remote_configuration, test_session
|
67
|
+
def configure(remote_configuration, test_session)
|
68
|
+
return unless enabled?
|
69
|
+
|
69
70
|
Datadog.logger.debug("Configuring TestOptimisation with remote configuration: #{remote_configuration}")
|
70
71
|
|
71
|
-
@enabled =
|
72
|
-
|
73
|
-
|
74
|
-
@test_skipping_enabled = @enabled && Utils::Parsing.convert_to_bool(
|
75
|
-
remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY, false)
|
76
|
-
)
|
77
|
-
@code_coverage_enabled = @enabled && Utils::Parsing.convert_to_bool(
|
78
|
-
remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY, false)
|
79
|
-
)
|
72
|
+
@enabled = remote_configuration.itr_enabled?
|
73
|
+
@test_skipping_enabled = @enabled && remote_configuration.tests_skipping_enabled?
|
74
|
+
@code_coverage_enabled = @enabled && remote_configuration.code_coverage_enabled?
|
80
75
|
|
81
76
|
test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED, @test_skipping_enabled)
|
82
|
-
# currently we set this tag when ITR requires collecting code coverage
|
83
|
-
# this will change as soon as we implement total code coverage support in this library
|
84
77
|
test_session.set_tag(Ext::Test::TAG_CODE_COVERAGE_ENABLED, @code_coverage_enabled)
|
85
|
-
|
86
78
|
# we skip tests, not suites
|
87
79
|
test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, Ext::Test::ITR_TEST_SKIPPING_MODE)
|
88
80
|
|
@@ -90,7 +82,7 @@ module Datadog
|
|
90
82
|
|
91
83
|
Datadog.logger.debug("Configured TestOptimisation with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}")
|
92
84
|
|
93
|
-
fetch_skippable_tests(test_session
|
85
|
+
fetch_skippable_tests(test_session)
|
94
86
|
end
|
95
87
|
|
96
88
|
def enabled?
|
@@ -225,7 +217,7 @@ module Datadog
|
|
225
217
|
coverage[absolute_test_source_file_path] = true
|
226
218
|
end
|
227
219
|
|
228
|
-
def fetch_skippable_tests(test_session
|
220
|
+
def fetch_skippable_tests(test_session)
|
229
221
|
return unless skipping_tests?
|
230
222
|
|
231
223
|
# we can only request skippable tests if git metadata is already uploaded
|
@@ -248,6 +240,10 @@ module Datadog
|
|
248
240
|
def code_coverage_mode
|
249
241
|
@use_single_threaded_coverage ? :single : :multi
|
250
242
|
end
|
243
|
+
|
244
|
+
def git_tree_upload_worker
|
245
|
+
Datadog.send(:components).git_tree_upload_worker
|
246
|
+
end
|
251
247
|
end
|
252
248
|
end
|
253
249
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "strategy/no_retry"
|
4
|
+
require_relative "strategy/retry_failed"
|
5
|
+
|
6
|
+
module Datadog
|
7
|
+
module CI
|
8
|
+
module TestRetries
|
9
|
+
# Encapsulates the logic to enable test retries, including:
|
10
|
+
# - retrying failed tests - improve success rate of CI pipelines
|
11
|
+
# - retrying new tests - detect flaky tests as early as possible to prevent them from being merged
|
12
|
+
class Component
|
13
|
+
attr_reader :retry_failed_tests_enabled, :retry_failed_tests_max_attempts,
|
14
|
+
:retry_failed_tests_total_limit, :retry_failed_tests_count
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
retry_failed_tests_enabled:,
|
18
|
+
retry_failed_tests_max_attempts:,
|
19
|
+
retry_failed_tests_total_limit:
|
20
|
+
)
|
21
|
+
@retry_failed_tests_enabled = retry_failed_tests_enabled
|
22
|
+
@retry_failed_tests_max_attempts = retry_failed_tests_max_attempts
|
23
|
+
@retry_failed_tests_total_limit = retry_failed_tests_total_limit
|
24
|
+
# counter that store the current number of failed tests retried
|
25
|
+
@retry_failed_tests_count = 0
|
26
|
+
|
27
|
+
@mutex = Mutex.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def configure(library_settings)
|
31
|
+
@retry_failed_tests_enabled &&= library_settings.flaky_test_retries_enabled?
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_retries(&block)
|
35
|
+
# @type var retry_strategy: Strategy::Base
|
36
|
+
retry_strategy = nil
|
37
|
+
|
38
|
+
test_finished_callback = lambda do |test_span|
|
39
|
+
if retry_strategy.nil?
|
40
|
+
# we always run test at least once and after first pass create a correct retry strategy
|
41
|
+
retry_strategy = build_strategy(test_span)
|
42
|
+
else
|
43
|
+
# after each retry we record the result, strategy will decide if we should retry again
|
44
|
+
retry_strategy&.record_retry(test_span)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
test_visibility_component.set_test_finished_callback(test_finished_callback)
|
49
|
+
|
50
|
+
loop do
|
51
|
+
yield
|
52
|
+
|
53
|
+
break unless retry_strategy&.should_retry?
|
54
|
+
end
|
55
|
+
ensure
|
56
|
+
test_visibility_component.remove_test_finished_callback
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_strategy(test_span)
|
60
|
+
@mutex.synchronize do
|
61
|
+
if should_retry_failed_test?(test_span)
|
62
|
+
Datadog.logger.debug("Failed test retry starts")
|
63
|
+
@retry_failed_tests_count += 1
|
64
|
+
|
65
|
+
Strategy::RetryFailed.new(max_attempts: @retry_failed_tests_max_attempts)
|
66
|
+
else
|
67
|
+
Strategy::NoRetry.new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def should_retry_failed_test?(test_span)
|
75
|
+
@retry_failed_tests_enabled && !!test_span&.failed? && @retry_failed_tests_count < @retry_failed_tests_total_limit
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_visibility_component
|
79
|
+
Datadog.send(:components).test_visibility
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "component"
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
module CI
|
7
|
+
module TestRetries
|
8
|
+
class NullComponent < Component
|
9
|
+
attr_reader :retry_failed_tests_enabled, :retry_failed_tests_max_attempts, :retry_failed_tests_total_limit
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
# enabled only by remote settings
|
13
|
+
@retry_failed_tests_enabled = false
|
14
|
+
@retry_failed_tests_max_attempts = 0
|
15
|
+
@retry_failed_tests_total_limit = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure(library_settings)
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_retries(&block)
|
22
|
+
no_action = proc {}
|
23
|
+
yield no_action
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module CI
|
5
|
+
module TestRetries
|
6
|
+
module Strategy
|
7
|
+
class Base
|
8
|
+
def should_retry?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def record_retry(test_span)
|
13
|
+
test_span&.set_tag(Ext::Test::TAG_IS_RETRY, "true")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
require_relative "../../ext/test"
|
6
|
+
|
7
|
+
module Datadog
|
8
|
+
module CI
|
9
|
+
module TestRetries
|
10
|
+
module Strategy
|
11
|
+
class RetryFailed < Base
|
12
|
+
attr_reader :max_attempts
|
13
|
+
|
14
|
+
def initialize(max_attempts:)
|
15
|
+
@max_attempts = max_attempts
|
16
|
+
|
17
|
+
@attempts = 0
|
18
|
+
@passed_once = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def should_retry?
|
22
|
+
@attempts < @max_attempts && !@passed_once
|
23
|
+
end
|
24
|
+
|
25
|
+
def record_retry(test_span)
|
26
|
+
super
|
27
|
+
|
28
|
+
@attempts += 1
|
29
|
+
@passed_once = true if test_span&.passed?
|
30
|
+
|
31
|
+
Datadog.logger.debug { "Retry Attempts [#{@attempts} / #{@max_attempts}], Passed: [#{@passed_once}]" }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -16,7 +16,9 @@ module Datadog
|
|
16
16
|
def initialize(tracer_span)
|
17
17
|
super
|
18
18
|
|
19
|
-
|
19
|
+
# counts how many times every test in this suite was executed with each status:
|
20
|
+
# { "MySuite.mytest.a:1" => { "pass" => 3, "fail" => 2 } }
|
21
|
+
@execution_stats_per_test = {}
|
20
22
|
end
|
21
23
|
|
22
24
|
# Finishes this test suite.
|
@@ -33,42 +35,61 @@ module Datadog
|
|
33
35
|
end
|
34
36
|
|
35
37
|
# @internal
|
36
|
-
def record_test_result(datadog_test_status)
|
38
|
+
def record_test_result(test_id, datadog_test_status)
|
37
39
|
synchronize do
|
38
|
-
@
|
40
|
+
@execution_stats_per_test[test_id] ||= Hash.new(0)
|
41
|
+
@execution_stats_per_test[test_id][datadog_test_status] += 1
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
42
45
|
# @internal
|
43
|
-
def
|
46
|
+
def any_passed?
|
44
47
|
synchronize do
|
45
|
-
@
|
48
|
+
@execution_stats_per_test.any? do |_, stats|
|
49
|
+
stats[Ext::Test::Status::PASS] > 0
|
50
|
+
end
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
49
54
|
# @internal
|
50
|
-
def
|
55
|
+
def test_executed?(test_id)
|
51
56
|
synchronize do
|
52
|
-
@
|
57
|
+
@execution_stats_per_test.key?(test_id)
|
53
58
|
end
|
54
59
|
end
|
55
60
|
|
56
|
-
|
57
|
-
|
61
|
+
private
|
62
|
+
|
63
|
+
def set_status_from_stats!
|
58
64
|
synchronize do
|
59
|
-
|
65
|
+
# count how many tests passed, failed and skipped
|
66
|
+
test_suite_stats = @execution_stats_per_test.each_with_object(Hash.new(0)) do |(_test_id, stats), acc|
|
67
|
+
acc[derive_test_status_from_execution_stats(stats)] += 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# test suite is considered failed if at least one test failed
|
71
|
+
if test_suite_stats[Ext::Test::Status::FAIL] > 0
|
72
|
+
failed!
|
73
|
+
# if there are no failures and no passes, it is skipped
|
74
|
+
elsif test_suite_stats[Ext::Test::Status::PASS] == 0
|
75
|
+
skipped!
|
76
|
+
# otherwise we consider it passed
|
77
|
+
else
|
78
|
+
passed!
|
79
|
+
end
|
60
80
|
end
|
61
81
|
end
|
62
82
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
elsif
|
69
|
-
|
83
|
+
def derive_test_status_from_execution_stats(test_execution_stats)
|
84
|
+
# test is passed if it passed at least once
|
85
|
+
if test_execution_stats[Ext::Test::Status::PASS] > 0
|
86
|
+
Ext::Test::Status::PASS
|
87
|
+
# if test was never passed, it is failed if it failed at least once
|
88
|
+
elsif test_execution_stats[Ext::Test::Status::FAIL] > 0
|
89
|
+
Ext::Test::Status::FAIL
|
90
|
+
# otherwise it is skipped
|
70
91
|
else
|
71
|
-
|
92
|
+
Ext::Test::Status::SKIP
|
72
93
|
end
|
73
94
|
end
|
74
95
|
end
|