datadog-ci 1.3.0 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|