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