datadog-ci 1.4.1 → 1.5.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 +17 -2
- data/lib/datadog/ci/configuration/components.rb +12 -6
- data/lib/datadog/ci/configuration/settings.rb +6 -0
- data/lib/datadog/ci/contrib/cucumber/filter.rb +40 -0
- data/lib/datadog/ci/contrib/cucumber/instrumentation.rb +15 -1
- data/lib/datadog/ci/contrib/cucumber/patcher.rb +0 -2
- data/lib/datadog/ci/contrib/minitest/runner.rb +6 -1
- data/lib/datadog/ci/contrib/minitest/test.rb +4 -0
- data/lib/datadog/ci/contrib/rspec/example.rb +11 -10
- data/lib/datadog/ci/contrib/rspec/runner.rb +2 -1
- data/lib/datadog/ci/ext/settings.rb +1 -0
- data/lib/datadog/ci/ext/telemetry.rb +9 -0
- data/lib/datadog/ci/ext/test.rb +6 -1
- data/lib/datadog/ci/ext/transport.rb +7 -0
- data/lib/datadog/ci/remote/component.rb +13 -2
- data/lib/datadog/ci/remote/library_settings.rb +48 -7
- data/lib/datadog/ci/remote/library_settings_client.rb +2 -1
- data/lib/datadog/ci/remote/slow_test_retries.rb +53 -0
- data/lib/datadog/ci/test.rb +15 -2
- data/lib/datadog/ci/test_optimisation/component.rb +9 -6
- data/lib/datadog/ci/test_optimisation/skippable.rb +1 -1
- data/lib/datadog/ci/test_retries/component.rb +68 -39
- data/lib/datadog/ci/test_retries/driver/base.rb +25 -0
- data/lib/datadog/ci/test_retries/driver/no_retry.rb +16 -0
- data/lib/datadog/ci/test_retries/driver/retry_failed.rb +37 -0
- data/lib/datadog/ci/test_retries/driver/retry_new.rb +50 -0
- data/lib/datadog/ci/test_retries/null_component.rb +7 -6
- data/lib/datadog/ci/test_retries/strategy/base.rb +11 -4
- data/lib/datadog/ci/test_retries/strategy/no_retry.rb +0 -2
- data/lib/datadog/ci/test_retries/strategy/retry_failed.rb +30 -13
- data/lib/datadog/ci/test_retries/strategy/retry_new.rb +132 -0
- data/lib/datadog/ci/test_retries/unique_tests_client.rb +132 -0
- data/lib/datadog/ci/test_session.rb +2 -0
- data/lib/datadog/ci/test_suite.rb +8 -0
- data/lib/datadog/ci/test_visibility/component.rb +23 -13
- data/lib/datadog/ci/test_visibility/null_component.rb +1 -1
- data/lib/datadog/ci/test_visibility/telemetry.rb +9 -0
- data/lib/datadog/ci/utils/test_run.rb +1 -1
- data/lib/datadog/ci/version.rb +2 -2
- data/lib/datadog/ci.rb +6 -3
- metadata +11 -5
- data/lib/datadog/ci/contrib/cucumber/configuration_override.rb +0 -37
- data/lib/datadog/ci/utils/identity.rb +0 -20
@@ -1,7 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "driver/no_retry"
|
4
|
+
require_relative "driver/retry_failed"
|
5
|
+
require_relative "driver/retry_new"
|
6
|
+
|
3
7
|
require_relative "strategy/no_retry"
|
4
8
|
require_relative "strategy/retry_failed"
|
9
|
+
require_relative "strategy/retry_new"
|
10
|
+
|
11
|
+
require_relative "../ext/telemetry"
|
12
|
+
require_relative "../utils/telemetry"
|
5
13
|
|
6
14
|
module Datadog
|
7
15
|
module CI
|
@@ -10,73 +18,94 @@ module Datadog
|
|
10
18
|
# - retrying failed tests - improve success rate of CI pipelines
|
11
19
|
# - retrying new tests - detect flaky tests as early as possible to prevent them from being merged
|
12
20
|
class Component
|
13
|
-
|
14
|
-
:retry_failed_tests_total_limit, :retry_failed_tests_count
|
21
|
+
FIBER_LOCAL_CURRENT_RETRY_DRIVER_KEY = :__dd_current_retry_driver
|
15
22
|
|
16
23
|
def initialize(
|
17
24
|
retry_failed_tests_enabled:,
|
18
25
|
retry_failed_tests_max_attempts:,
|
19
|
-
retry_failed_tests_total_limit
|
26
|
+
retry_failed_tests_total_limit:,
|
27
|
+
retry_new_tests_enabled:,
|
28
|
+
unique_tests_client:
|
20
29
|
)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
30
|
+
no_retries_strategy = Strategy::NoRetry.new
|
31
|
+
|
32
|
+
retry_failed_strategy = Strategy::RetryFailed.new(
|
33
|
+
enabled: retry_failed_tests_enabled,
|
34
|
+
max_attempts: retry_failed_tests_max_attempts,
|
35
|
+
total_limit: retry_failed_tests_total_limit
|
36
|
+
)
|
26
37
|
|
38
|
+
retry_new_strategy = Strategy::RetryNew.new(
|
39
|
+
enabled: retry_new_tests_enabled,
|
40
|
+
unique_tests_client: unique_tests_client
|
41
|
+
)
|
42
|
+
|
43
|
+
# order is important, we should try to retry new tests first
|
44
|
+
@retry_strategies = [retry_new_strategy, retry_failed_strategy, no_retries_strategy]
|
27
45
|
@mutex = Mutex.new
|
28
46
|
end
|
29
47
|
|
30
|
-
def configure(library_settings)
|
31
|
-
|
48
|
+
def configure(library_settings, test_session)
|
49
|
+
# let all strategies configure themselves
|
50
|
+
@retry_strategies.each do |strategy|
|
51
|
+
strategy.configure(library_settings, test_session)
|
52
|
+
end
|
32
53
|
end
|
33
54
|
|
34
55
|
def with_retries(&block)
|
35
|
-
|
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)
|
56
|
+
reset_retries!
|
49
57
|
|
50
58
|
loop do
|
51
59
|
yield
|
52
60
|
|
53
|
-
break unless
|
61
|
+
break unless should_retry?
|
54
62
|
end
|
55
63
|
ensure
|
56
|
-
|
64
|
+
reset_retries!
|
57
65
|
end
|
58
66
|
|
59
|
-
def
|
67
|
+
def build_driver(test_span)
|
60
68
|
@mutex.synchronize do
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
Strategy::NoRetry.new
|
68
|
-
end
|
69
|
+
# find the first strategy that covers the test span and let it build the driver
|
70
|
+
strategy = @retry_strategies.find { |strategy| strategy.covers?(test_span) }
|
71
|
+
|
72
|
+
raise "No retry strategy found for test span: #{test_span.name}" if strategy.nil?
|
73
|
+
|
74
|
+
strategy.build_driver(test_span)
|
69
75
|
end
|
70
76
|
end
|
71
77
|
|
78
|
+
def record_test_finished(test_span)
|
79
|
+
if current_retry_driver.nil?
|
80
|
+
# we always run test at least once and after the first pass create a correct retry driver
|
81
|
+
self.current_retry_driver = build_driver(test_span)
|
82
|
+
else
|
83
|
+
# after each retry we record the result, the driver will decide if we should retry again
|
84
|
+
current_retry_driver&.record_retry(test_span)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def record_test_span_duration(tracer_span)
|
89
|
+
current_retry_driver&.record_duration(tracer_span.duration)
|
90
|
+
end
|
91
|
+
|
92
|
+
# this API is targeted on Cucumber instrumentation or any other that cannot leverage #with_retries method
|
93
|
+
def reset_retries!
|
94
|
+
self.current_retry_driver = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def should_retry?
|
98
|
+
!!current_retry_driver&.should_retry?
|
99
|
+
end
|
100
|
+
|
72
101
|
private
|
73
102
|
|
74
|
-
def
|
75
|
-
|
103
|
+
def current_retry_driver
|
104
|
+
Thread.current[FIBER_LOCAL_CURRENT_RETRY_DRIVER_KEY]
|
76
105
|
end
|
77
106
|
|
78
|
-
def
|
79
|
-
|
107
|
+
def current_retry_driver=(driver)
|
108
|
+
Thread.current[FIBER_LOCAL_CURRENT_RETRY_DRIVER_KEY] = driver
|
80
109
|
end
|
81
110
|
end
|
82
111
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module CI
|
5
|
+
module TestRetries
|
6
|
+
module Driver
|
7
|
+
# Driver is the class responsible for the current test retry mechanism.
|
8
|
+
# It receives signals about each retry execution and steers the current retry strategy.
|
9
|
+
class Base
|
10
|
+
def should_retry?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def record_retry(test_span)
|
15
|
+
test_span&.set_tag(Ext::Test::TAG_IS_RETRY, "true")
|
16
|
+
end
|
17
|
+
|
18
|
+
# duration in float seconds
|
19
|
+
def record_duration(duration)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
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 Driver
|
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
|
@@ -0,0 +1,50 @@
|
|
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 Driver
|
11
|
+
# retry every new test up to 10 times (early flake detection)
|
12
|
+
class RetryNew < Base
|
13
|
+
def initialize(test_span, max_attempts_thresholds:)
|
14
|
+
@max_attempts_thresholds = max_attempts_thresholds
|
15
|
+
@attempts = 0
|
16
|
+
# will be changed based on test span duration
|
17
|
+
@max_attempts = 10
|
18
|
+
|
19
|
+
mark_new_test(test_span)
|
20
|
+
end
|
21
|
+
|
22
|
+
def should_retry?
|
23
|
+
@attempts < @max_attempts
|
24
|
+
end
|
25
|
+
|
26
|
+
def record_retry(test_span)
|
27
|
+
super
|
28
|
+
|
29
|
+
@attempts += 1
|
30
|
+
mark_new_test(test_span)
|
31
|
+
|
32
|
+
Datadog.logger.debug { "Retry Attempts [#{@attempts} / #{@max_attempts}]" }
|
33
|
+
end
|
34
|
+
|
35
|
+
def record_duration(duration)
|
36
|
+
@max_attempts = @max_attempts_thresholds.max_attempts_for_duration(duration)
|
37
|
+
|
38
|
+
Datadog.logger.debug { "Recorded test duration of [#{duration}], new Max Attempts value is [#{@max_attempts}]" }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def mark_new_test(test_span)
|
44
|
+
test_span.set_tag(Ext::Test::TAG_IS_NEW, "true")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -6,13 +6,7 @@ module Datadog
|
|
6
6
|
module CI
|
7
7
|
module TestRetries
|
8
8
|
class NullComponent < Component
|
9
|
-
attr_reader :retry_failed_tests_enabled, :retry_failed_tests_max_attempts, :retry_failed_tests_total_limit
|
10
|
-
|
11
9
|
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
10
|
end
|
17
11
|
|
18
12
|
def configure(library_settings)
|
@@ -22,6 +16,13 @@ module Datadog
|
|
22
16
|
no_action = proc {}
|
23
17
|
yield no_action
|
24
18
|
end
|
19
|
+
|
20
|
+
def reset_retries!
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_retry?
|
24
|
+
false
|
25
|
+
end
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -1,16 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../driver/no_retry"
|
4
|
+
|
3
5
|
module Datadog
|
4
6
|
module CI
|
5
7
|
module TestRetries
|
6
8
|
module Strategy
|
9
|
+
# Strategies are subcomponents of the retry mechanism. They are responsible for
|
10
|
+
# determining which tests should be retried and how.
|
7
11
|
class Base
|
8
|
-
def
|
9
|
-
|
12
|
+
def covers?(test_span)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def configure(_library_settings, _test_session)
|
10
17
|
end
|
11
18
|
|
12
|
-
def
|
13
|
-
|
19
|
+
def build_driver(_test_span)
|
20
|
+
Driver::NoRetry.new
|
14
21
|
end
|
15
22
|
end
|
16
23
|
end
|
@@ -2,33 +2,50 @@
|
|
2
2
|
|
3
3
|
require_relative "base"
|
4
4
|
|
5
|
-
require_relative "
|
5
|
+
require_relative "../driver/retry_failed"
|
6
6
|
|
7
7
|
module Datadog
|
8
8
|
module CI
|
9
9
|
module TestRetries
|
10
10
|
module Strategy
|
11
11
|
class RetryFailed < Base
|
12
|
-
attr_reader :max_attempts
|
13
|
-
|
14
|
-
|
12
|
+
attr_reader :enabled, :max_attempts,
|
13
|
+
:total_limit, :retried_count
|
14
|
+
|
15
|
+
def initialize(
|
16
|
+
enabled:,
|
17
|
+
max_attempts:,
|
18
|
+
total_limit:
|
19
|
+
)
|
20
|
+
@enabled = enabled
|
15
21
|
@max_attempts = max_attempts
|
22
|
+
@total_limit = total_limit
|
23
|
+
@retried_count = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def covers?(test_span)
|
27
|
+
return false unless @enabled
|
28
|
+
|
29
|
+
if @retried_count >= @total_limit
|
30
|
+
Datadog.logger.debug do
|
31
|
+
"Retry failed tests limit reached: [#{@retried_count}] out of [#{@total_limit}]"
|
32
|
+
end
|
33
|
+
@enabled = false
|
34
|
+
end
|
16
35
|
|
17
|
-
@
|
18
|
-
@passed_once = false
|
36
|
+
@enabled && !!test_span&.failed?
|
19
37
|
end
|
20
38
|
|
21
|
-
def
|
22
|
-
@
|
39
|
+
def configure(library_settings, test_session)
|
40
|
+
@enabled &&= library_settings.flaky_test_retries_enabled?
|
23
41
|
end
|
24
42
|
|
25
|
-
def
|
26
|
-
|
43
|
+
def build_driver(test_span)
|
44
|
+
Datadog.logger.debug { "#{test_span.name} failed, will be retried" }
|
27
45
|
|
28
|
-
@
|
29
|
-
@passed_once = true if test_span&.passed?
|
46
|
+
@retried_count += 1
|
30
47
|
|
31
|
-
|
48
|
+
Driver::RetryFailed.new(max_attempts: max_attempts)
|
32
49
|
end
|
33
50
|
end
|
34
51
|
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
require_relative "../driver/retry_new"
|
6
|
+
|
7
|
+
module Datadog
|
8
|
+
module CI
|
9
|
+
module TestRetries
|
10
|
+
module Strategy
|
11
|
+
class RetryNew < Base
|
12
|
+
DEFAULT_TOTAL_TESTS_COUNT = 100
|
13
|
+
|
14
|
+
attr_reader :enabled, :max_attempts_thresholds, :unique_tests_set, :total_limit, :retried_count
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
enabled:,
|
18
|
+
unique_tests_client:
|
19
|
+
)
|
20
|
+
@enabled = enabled
|
21
|
+
@unique_tests_set = Set.new
|
22
|
+
# total maximum number of new tests to retry (will be set based on the total number of tests in the session)
|
23
|
+
@total_limit = 0
|
24
|
+
@retried_count = 0
|
25
|
+
|
26
|
+
@unique_tests_client = unique_tests_client
|
27
|
+
end
|
28
|
+
|
29
|
+
def covers?(test_span)
|
30
|
+
return false unless @enabled
|
31
|
+
|
32
|
+
if @retried_count >= @total_limit
|
33
|
+
Datadog.logger.debug do
|
34
|
+
"Retry new tests limit reached: [#{@retried_count}] out of [#{@total_limit}]"
|
35
|
+
end
|
36
|
+
@enabled = false
|
37
|
+
mark_test_session_faulty(Datadog::CI.active_test_session)
|
38
|
+
end
|
39
|
+
|
40
|
+
@enabled && !test_span.skipped? && is_new_test?(test_span)
|
41
|
+
end
|
42
|
+
|
43
|
+
def configure(library_settings, test_session)
|
44
|
+
@enabled &&= library_settings.early_flake_detection_enabled?
|
45
|
+
|
46
|
+
return unless @enabled
|
47
|
+
|
48
|
+
# mark early flake detection enabled for test session
|
49
|
+
test_session.set_tag(Ext::Test::TAG_EARLY_FLAKE_ENABLED, "true")
|
50
|
+
|
51
|
+
set_max_attempts_thresholds(library_settings)
|
52
|
+
calculate_total_retries_limit(library_settings, test_session)
|
53
|
+
fetch_known_unique_tests(test_session)
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_driver(test_span)
|
57
|
+
Datadog.logger.debug do
|
58
|
+
"#{test_span.name} is new, will be retried"
|
59
|
+
end
|
60
|
+
@retried_count += 1
|
61
|
+
|
62
|
+
Driver::RetryNew.new(test_span, max_attempts_thresholds: @max_attempts_thresholds)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def mark_test_session_faulty(test_session)
|
68
|
+
test_session&.set_tag(Ext::Test::TAG_EARLY_FLAKE_ABORT_REASON, Ext::Test::EARLY_FLAKE_FAULTY)
|
69
|
+
end
|
70
|
+
|
71
|
+
def is_new_test?(test_span)
|
72
|
+
test_id = Utils::TestRun.datadog_test_id(test_span.name, test_span.test_suite_name)
|
73
|
+
|
74
|
+
result = !@unique_tests_set.include?(test_id)
|
75
|
+
|
76
|
+
if result
|
77
|
+
Datadog.logger.debug do
|
78
|
+
"#{test_id} is not found in the unique tests set, it is a new test"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
def set_max_attempts_thresholds(library_settings)
|
86
|
+
@max_attempts_thresholds = library_settings.slow_test_retries
|
87
|
+
Datadog.logger.debug do
|
88
|
+
"Slow test retries thresholds: #{@max_attempts_thresholds.entries}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def calculate_total_retries_limit(library_settings, test_session)
|
93
|
+
percentage_limit = library_settings.faulty_session_threshold
|
94
|
+
tests_count = test_session.total_tests_count.to_i
|
95
|
+
if tests_count.zero?
|
96
|
+
Datadog.logger.debug do
|
97
|
+
"Total tests count is zero, using default value for the total number of tests: [#{DEFAULT_TOTAL_TESTS_COUNT}]"
|
98
|
+
end
|
99
|
+
|
100
|
+
tests_count = DEFAULT_TOTAL_TESTS_COUNT
|
101
|
+
end
|
102
|
+
@total_limit = (tests_count * percentage_limit / 100.0).ceil
|
103
|
+
Datadog.logger.debug do
|
104
|
+
"Retry new tests total limit is [#{@total_limit}] (#{percentage_limit}%) of #{tests_count}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def fetch_known_unique_tests(test_session)
|
109
|
+
@unique_tests_set = @unique_tests_client.fetch_unique_tests(test_session)
|
110
|
+
if @unique_tests_set.empty?
|
111
|
+
@enabled = false
|
112
|
+
mark_test_session_faulty(test_session)
|
113
|
+
|
114
|
+
Datadog.logger.warn(
|
115
|
+
"Disabling early flake detection because there are no known tests (possible reason: no test runs in default branch)"
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
# report how many unique tests were found
|
120
|
+
Datadog.logger.debug do
|
121
|
+
"Found [#{@unique_tests_set.size}] known unique tests"
|
122
|
+
end
|
123
|
+
Utils::Telemetry.distribution(
|
124
|
+
Ext::Telemetry::METRIC_EFD_UNIQUE_TESTS_RESPONSE_TESTS,
|
125
|
+
@unique_tests_set.size.to_f
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,132 @@
|
|
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/telemetry"
|
9
|
+
require_relative "../utils/test_run"
|
10
|
+
|
11
|
+
module Datadog
|
12
|
+
module CI
|
13
|
+
module TestRetries
|
14
|
+
# fetch a list of unique known tests from the backend
|
15
|
+
class UniqueTestsClient
|
16
|
+
class Response
|
17
|
+
def initialize(http_response)
|
18
|
+
@http_response = http_response
|
19
|
+
@json = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def ok?
|
23
|
+
resp = @http_response
|
24
|
+
!resp.nil? && resp.ok?
|
25
|
+
end
|
26
|
+
|
27
|
+
def tests
|
28
|
+
res = Set.new
|
29
|
+
|
30
|
+
payload
|
31
|
+
.fetch("data", {})
|
32
|
+
.fetch("attributes", {})
|
33
|
+
.fetch("tests", {})
|
34
|
+
.each do |_test_module, suites_hash|
|
35
|
+
suites_hash.each do |test_suite, tests|
|
36
|
+
tests.each do |test_name|
|
37
|
+
res << Utils::TestRun.datadog_test_id(test_name, test_suite)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
res
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def payload
|
48
|
+
cached = @json
|
49
|
+
return cached unless cached.nil?
|
50
|
+
|
51
|
+
resp = @http_response
|
52
|
+
return @json = {} if resp.nil? || !ok?
|
53
|
+
|
54
|
+
begin
|
55
|
+
@json = JSON.parse(resp.payload)
|
56
|
+
rescue JSON::ParserError => e
|
57
|
+
Datadog.logger.error("Failed to parse unique known tests response payload: #{e}. Payload was: #{resp.payload}")
|
58
|
+
@json = {}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(dd_env:, api: nil, config_tags: {})
|
64
|
+
@api = api
|
65
|
+
@dd_env = dd_env
|
66
|
+
@config_tags = config_tags
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_unique_tests(test_session)
|
70
|
+
api = @api
|
71
|
+
return Set.new unless api
|
72
|
+
|
73
|
+
request_payload = payload(test_session)
|
74
|
+
Datadog.logger.debug("Fetching unique known tests with request: #{request_payload}")
|
75
|
+
|
76
|
+
http_response = api.api_request(
|
77
|
+
path: Ext::Transport::DD_API_UNIQUE_TESTS_PATH,
|
78
|
+
payload: request_payload
|
79
|
+
)
|
80
|
+
|
81
|
+
Transport::Telemetry.api_requests(
|
82
|
+
Ext::Telemetry::METRIC_EFD_UNIQUE_TESTS_REQUEST,
|
83
|
+
1,
|
84
|
+
compressed: http_response.request_compressed
|
85
|
+
)
|
86
|
+
Utils::Telemetry.distribution(Ext::Telemetry::METRIC_EFD_UNIQUE_TESTS_REQUEST_MS, http_response.duration_ms)
|
87
|
+
Utils::Telemetry.distribution(
|
88
|
+
Ext::Telemetry::METRIC_EFD_UNIQUE_TESTS_RESPONSE_BYTES,
|
89
|
+
http_response.response_size.to_f,
|
90
|
+
{Ext::Telemetry::TAG_RESPONSE_COMPRESSED => http_response.gzipped_content?.to_s}
|
91
|
+
)
|
92
|
+
|
93
|
+
unless http_response.ok?
|
94
|
+
Transport::Telemetry.api_requests_errors(
|
95
|
+
Ext::Telemetry::METRIC_EFD_UNIQUE_TESTS_REQUEST_ERRORS,
|
96
|
+
1,
|
97
|
+
error_type: http_response.telemetry_error_type,
|
98
|
+
status_code: http_response.code
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
Response.new(http_response).tests
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def payload(test_session)
|
108
|
+
{
|
109
|
+
"data" => {
|
110
|
+
"id" => Datadog::Core::Environment::Identity.id,
|
111
|
+
"type" => Ext::Transport::DD_API_UNIQUE_TESTS_TYPE,
|
112
|
+
"attributes" => {
|
113
|
+
"repository_url" => test_session.git_repository_url,
|
114
|
+
"service" => test_session.service,
|
115
|
+
"env" => @dd_env,
|
116
|
+
"sha" => test_session.git_commit_sha,
|
117
|
+
"configurations" => {
|
118
|
+
Ext::Test::TAG_OS_PLATFORM => test_session.os_platform,
|
119
|
+
Ext::Test::TAG_OS_ARCHITECTURE => test_session.os_architecture,
|
120
|
+
Ext::Test::TAG_OS_VERSION => test_session.os_version,
|
121
|
+
Ext::Test::TAG_RUNTIME_NAME => test_session.runtime_name,
|
122
|
+
Ext::Test::TAG_RUNTIME_VERSION => test_session.runtime_version,
|
123
|
+
"custom" => @config_tags
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
}.to_json
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|