Class: Datadog::CI::TestRetries::Component

Inherits:
Object
  • Object
show all
Defined in:
lib/datadog/ci/test_retries/component.rb

Overview

Encapsulates the logic to enable test retries, including:

  • retrying failed tests - improve success rate of CI pipelines

  • retrying new tests - detect flaky tests as early as possible to prevent them from being merged

Direct Known Subclasses

NullComponent

Constant Summary collapse

FIBER_LOCAL_CURRENT_RETRY_DRIVER_KEY =
:__dd_current_retry_driver

Instance Method Summary collapse

Constructor Details

#initialize(retry_failed_tests_enabled:, retry_failed_tests_max_attempts:, retry_failed_tests_total_limit:, retry_new_tests_enabled:, retry_flaky_fixed_tests_enabled:, retry_flaky_fixed_tests_max_attempts:) ⇒ Component

Returns a new instance of Component.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/datadog/ci/test_retries/component.rb', line 24

def initialize(
  retry_failed_tests_enabled:,
  retry_failed_tests_max_attempts:,
  retry_failed_tests_total_limit:,
  retry_new_tests_enabled:,
  retry_flaky_fixed_tests_enabled:,
  retry_flaky_fixed_tests_max_attempts:
)
  no_retries_strategy = Strategy::NoRetry.new

  @retry_failed_strategy = Strategy::RetryFailed.new(
    enabled: retry_failed_tests_enabled,
    max_attempts: retry_failed_tests_max_attempts,
    total_limit: retry_failed_tests_total_limit
  )

  @retry_new_strategy = Strategy::RetryNew.new(
    enabled: retry_new_tests_enabled
  )

  retry_flaky_fixed_strategy = Strategy::RetryFlakyFixed.new(
    enabled: retry_flaky_fixed_tests_enabled,
    max_attempts: retry_flaky_fixed_tests_max_attempts
  )

  # order is important, we apply the first matching strategy
  @retry_strategies = [
    retry_flaky_fixed_strategy,
    @retry_new_strategy,
    @retry_failed_strategy,
    no_retries_strategy
  ]
  @mutex = Mutex.new
end

Instance Method Details

#auto_test_retries_feature_enabledObject



123
124
125
# File 'lib/datadog/ci/test_retries/component.rb', line 123

def auto_test_retries_feature_enabled
  @retry_failed_strategy.enabled
end

#build_driver(test_span) ⇒ Object



78
79
80
81
82
83
84
85
86
87
# File 'lib/datadog/ci/test_retries/component.rb', line 78

def build_driver(test_span)
  @mutex.synchronize do
    # find the first strategy that covers the test span and let it build the driver
    strategy = @retry_strategies.find { |strategy| strategy.covers?(test_span) }

    raise "No retry strategy found for test span: #{test_span.name}" if strategy.nil?

    strategy.build_driver(test_span)
  end
end

#configure(library_settings, test_session) ⇒ Object



59
60
61
62
63
64
# File 'lib/datadog/ci/test_retries/component.rb', line 59

def configure(library_settings, test_session)
  # let all strategies configure themselves
  @retry_strategies.each do |strategy|
    strategy.configure(library_settings, test_session)
  end
end

#early_flake_detection_feature_enabledObject



127
128
129
# File 'lib/datadog/ci/test_retries/component.rb', line 127

def early_flake_detection_feature_enabled
  @retry_new_strategy.enabled
end

#record_test_finished(test_span) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
# File 'lib/datadog/ci/test_retries/component.rb', line 89

def record_test_finished(test_span)
  if current_retry_driver.nil?
    # we always run test at least once and after the first pass create a correct retry driver
    self.current_retry_driver = build_driver(test_span)
  else
    # after each retry we record the result, the driver will decide if we should retry again
    current_retry_driver&.record_retry(test_span)

    tag_last_retry(test_span) unless should_retry?
  end
end

#record_test_span_duration(tracer_span) ⇒ Object



101
102
103
# File 'lib/datadog/ci/test_retries/component.rb', line 101

def record_test_span_duration(tracer_span)
  current_retry_driver&.record_duration(tracer_span.duration)
end

#reset_retries!Object

this API is targeted on Cucumber instrumentation or any other that cannot leverage #with_retries method



106
107
108
# File 'lib/datadog/ci/test_retries/component.rb', line 106

def reset_retries!
  self.current_retry_driver = nil
end

#should_retry?Boolean

Returns:

  • (Boolean)


119
120
121
# File 'lib/datadog/ci/test_retries/component.rb', line 119

def should_retry?
  !!current_retry_driver&.should_retry?
end

#tag_last_retry(test_span) ⇒ Object



110
111
112
113
114
115
116
117
# File 'lib/datadog/ci/test_retries/component.rb', line 110

def tag_last_retry(test_span)
  test_span&.set_tag(Ext::Test::TAG_HAS_FAILED_ALL_RETRIES, "true") if test_span&.all_executions_failed?

  # if we are attempting to fix the test and all retries passed, we indicate that the fix might have worked
  if test_span&.attempt_to_fix? && test_span.all_executions_passed?
    test_span&.set_tag(Ext::Test::TAG_ATTEMPT_TO_FIX_PASSED, "true")
  end
end

#with_retries(&block) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/datadog/ci/test_retries/component.rb', line 66

def with_retries(&block)
  reset_retries!

  loop do
    yield

    break unless should_retry?
  end
ensure
  reset_retries!
end