Skip to content

Commit

Permalink
Merge pull request #212 from DataDog/anmarchenko/flaky_test_retries_c…
Browse files Browse the repository at this point in the history
…ucumber

[SDTEST-437] Retry failed tests for cucumber
  • Loading branch information
anmarchenko authored Aug 5, 2024
2 parents f00caf6 + 5e2a950 commit 2eb656d
Show file tree
Hide file tree
Showing 35 changed files with 614 additions and 84 deletions.
8 changes: 7 additions & 1 deletion lib/datadog/ci/configuration/components.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative "../test_optimisation/component"
require_relative "../test_optimisation/coverage/transport"
require_relative "../test_optimisation/coverage/writer"
require_relative "../test_retries/component"
require_relative "../test_visibility/component"
require_relative "../test_visibility/flush"
require_relative "../test_visibility/null_component"
Expand All @@ -27,13 +28,14 @@ module CI
module Configuration
# Adds CI behavior to Datadog trace components
module Components
attr_reader :test_visibility, :test_optimisation, :git_tree_upload_worker, :ci_remote
attr_reader :test_visibility, :test_optimisation, :git_tree_upload_worker, :ci_remote, :test_retries

def initialize(settings)
@test_optimisation = nil
@test_visibility = TestVisibility::NullComponent.new
@git_tree_upload_worker = DummyWorker.new
@ci_remote = nil
@test_retries = nil

# Activate CI mode if enabled
if settings.ci.enabled
Expand Down Expand Up @@ -110,6 +112,10 @@ def activate_ci!(settings)
@ci_remote = Remote::Component.new(
library_settings_client: build_library_settings_client(settings, test_visibility_api)
)
@test_retries = TestRetries::Component.new(
retry_failed_tests_max_attempts: settings.ci.retry_failed_tests_max_attempts,
retry_failed_tests_total_limit: settings.ci.retry_failed_tests_total_limit
)
# @type ivar @test_optimisation: Datadog::CI::TestOptimisation::Component
@test_optimisation = build_test_optimisation(settings, test_visibility_api)
@test_visibility = TestVisibility::Component.new(
Expand Down
12 changes: 12 additions & 0 deletions lib/datadog/ci/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ def self.add_settings!(base)
o.default true
end

option :retry_failed_tests_max_attempts do |o|
o.type :int
o.env CI::Ext::Settings::ENV_RETRY_FAILED_TESTS_MAX_ATTEMPTS
o.default 5
end

option :retry_failed_tests_total_limit do |o|
o.type :int
o.env CI::Ext::Settings::ENV_RETRY_FAILED_TESTS_TOTAL_LIMIT
o.default 100
end

define_method(:instrument) do |integration_name, options = {}, &block|
return unless enabled

Expand Down
37 changes: 37 additions & 0 deletions lib/datadog/ci/contrib/cucumber/configuration_override.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require_relative "formatter"

module Datadog
module CI
module Contrib
module Cucumber
# Changes behaviour of Cucumber::Configuration class
module ConfigurationOverride
def self.included(base)
base.prepend(InstanceMethods)
end

# Instance methods for configuration
module InstanceMethods
def retry_attempts
super if !datadog_test_retries_component&.retry_failed_tests_enabled

datadog_test_retries_component&.retry_failed_tests_max_attempts
end

def retry_total_tests
super if !datadog_test_retries_component&.retry_failed_tests_enabled

datadog_test_retries_component&.retry_failed_tests_total_limit
end

def datadog_test_retries_component
Datadog.send(:components).test_retries
end
end
end
end
end
end
end
10 changes: 5 additions & 5 deletions lib/datadog/ci/contrib/cucumber/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def initialize(config)

@current_test_suite = nil

@failed_tests_count = 0
@failed_test_suites_count = 0

bind_events(config)
end
Expand All @@ -46,10 +46,12 @@ def on_test_run_started(event)
end

def on_test_run_finished(event)
finish_current_test_suite

if event.respond_to?(:success)
finish_session(event.success)
else
finish_session(@failed_tests_count.zero?)
finish_session(@failed_test_suites_count.zero?)
end
end

Expand Down Expand Up @@ -86,7 +88,6 @@ def on_test_case_finished(event)
return if test_span.nil?

finish_span(test_span, event.result)
@failed_tests_count += 1 if test_span.failed?
end

def on_test_step_started(event)
Expand Down Expand Up @@ -128,8 +129,6 @@ def finish_span(span, result)
end

def finish_session(result)
finish_current_test_suite

test_session = test_visibility_component.active_test_session
test_module = test_visibility_component.active_test_module

Expand All @@ -155,6 +154,7 @@ def start_test_suite(test_suite_name)

def finish_current_test_suite
@current_test_suite&.finish
@failed_test_suites_count += 1 if @current_test_suite&.failed?

@current_test_suite = nil
end
Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/ci/contrib/cucumber/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Datadog
module CI
module Contrib
module Cucumber
# Instrumentation for Cucumber
# Instrumentation for Cucumber::Runtime class
module Instrumentation
def self.included(base)
base.prepend(InstanceMethods)
Expand Down
2 changes: 2 additions & 0 deletions lib/datadog/ci/contrib/cucumber/patcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "datadog/tracing/contrib/patcher"

require_relative "instrumentation"
require_relative "configuration_override"

module Datadog
module CI
Expand All @@ -20,6 +21,7 @@ def target_version

def patch
::Cucumber::Runtime.include(Instrumentation)
::Cucumber::Configuration.include(ConfigurationOverride)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/ci/contrib/rspec/example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def run(reporter = ::RSpec::Core::NullReporter)
success = super
return success unless test_suite

if success && test_suite.passed_tests_count > 0
if success && test_suite.any_passed?
test_suite.passed!
elsif success
test_suite.skipped!
Expand Down
2 changes: 2 additions & 0 deletions lib/datadog/ci/ext/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ module Settings
ENV_ITR_CODE_COVERAGE_EXCLUDED_BUNDLE_PATH = "DD_CIVISIBILITY_ITR_CODE_COVERAGE_EXCLUDED_BUNDLE_PATH"
ENV_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE = "DD_CIVISIBILITY_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE"
ENV_ITR_TEST_IMPACT_ANALYSIS_USE_ALLOCATION_TRACING = "DD_CIVISIBILITY_ITR_TEST_IMPACT_ANALYSIS_USE_ALLOCATION_TRACING"
ENV_RETRY_FAILED_TESTS_MAX_ATTEMPTS = "DD_CIVISIBILITY_RETRY_FAILED_TESTS_MAX_ATTEMPTS"
ENV_RETRY_FAILED_TESTS_TOTAL_LIMIT = "DD_CIVISIBILITY_RETRY_FAILED_TESTS_TOTAL_LIMIT"

# Source: https://docs.datadoghq.com/getting_started/site/
DD_SITE_ALLOWLIST = %w[
Expand Down
22 changes: 11 additions & 11 deletions lib/datadog/ci/ext/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Ext
module Test
CONTEXT_ORIGIN = "ciapp-test"

# Base test visibility tags
# Test visibility tags
TAG_FRAMEWORK = "test.framework"
TAG_FRAMEWORK_VERSION = "test.framework_version"
TAG_NAME = "test.name"
Expand All @@ -23,7 +23,8 @@ module Test
TAG_CODEOWNERS = "test.codeowners"
TAG_PARAMETERS = "test.parameters"

# ITR tags
# Test optimisation tags
TAG_CODE_COVERAGE_ENABLED = "test.code_coverage.enabled"
TAG_ITR_TEST_SKIPPING_ENABLED = "test.itr.tests_skipping.enabled"
TAG_ITR_TEST_SKIPPING_TYPE = "test.itr.tests_skipping.type"
TAG_ITR_TEST_SKIPPING_COUNT = "test.itr.tests_skipping.count"
Expand All @@ -32,11 +33,9 @@ module Test
TAG_ITR_UNSKIPPABLE = "test.itr.unskippable"
TAG_ITR_FORCED_RUN = "test.itr.forced_run"

# Code coverage tags
TAG_CODE_COVERAGE_ENABLED = "test.code_coverage.enabled"

# Special tags, not sent to the backend.
# these tags are special and used to correlate tests with the test sessions, suites, and modules
# Internal tags, they are not sent to the backend.
# These tags are internal to this library and used to correlate tests with
# the test sessions, suites, and modules.
TAG_TEST_SESSION_ID = "_test.session_id"
TAG_TEST_MODULE_ID = "_test.module_id"
TAG_TEST_SUITE_ID = "_test.suite_id"
Expand All @@ -50,8 +49,7 @@ module Test
TAG_RUNTIME_VERSION = "runtime.version"

# Tags for browser tests
# true if Datadog RUM was detected in the page(s) loaded by Selenium
TAG_IS_RUM_ACTIVE = "test.is_rum_active"
TAG_IS_RUM_ACTIVE = "test.is_rum_active" # true if Datadog RUM was detected in the page(s) loaded by Selenium
TAG_BROWSER_DRIVER = "test.browser.driver"
# version of selenium driver used
TAG_BROWSER_DRIVER_VERSION = "test.browser.driver_version"
Expand All @@ -60,6 +58,9 @@ module Test
# version of the browser, if multiple browsers or multiple versions then this tag is empty
TAG_BROWSER_VERSION = "test.browser.version"

# Tags for test retries
TAG_IS_RETRY = "test.is_retry" # true if test was retried by datadog-ci library

# internal APM tag to mark a span as a test span
TAG_SPAN_KIND = "span.kind"
SPAN_KIND_TEST = "test"
Expand All @@ -68,8 +69,7 @@ module Test
INHERITABLE_TAGS = [TAG_FRAMEWORK, TAG_FRAMEWORK_VERSION].freeze

# could be either "test" or "suite" depending on whether we skip individual tests or whole suites
# we use test skipping for Ruby
ITR_TEST_SKIPPING_MODE = "test"
ITR_TEST_SKIPPING_MODE = "test" # we always skip tests (not suites) in Ruby
ITR_TEST_SKIP_REASON = "Skipped by Datadog's intelligent test runner"
ITR_UNSKIPPABLE_OPTION = :datadog_itr_unskippable

Expand Down
1 change: 1 addition & 0 deletions lib/datadog/ci/ext/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module Transport
DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY = "code_coverage"
DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY = "tests_skipping"
DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY = "require_git"
DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY = "flaky_test_retries_enabled"
DD_API_SETTINGS_RESPONSE_DEFAULT = {DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY => false}.freeze

DD_API_GIT_SEARCH_COMMITS_PATH = "/api/v2/git/repository/search_commits"
Expand Down
5 changes: 5 additions & 0 deletions lib/datadog/ci/remote/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def configure(test_session)
end

test_optimisation.configure(library_configuration, test_session)
test_retries.configure(library_configuration)
end

private
Expand All @@ -36,6 +37,10 @@ def test_optimisation
Datadog.send(:components).test_optimisation
end

def test_retries
Datadog.send(:components).test_retries
end

def git_tree_upload_worker
Datadog.send(:components).git_tree_upload_worker
end
Expand Down
6 changes: 6 additions & 0 deletions lib/datadog/ci/remote/library_settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ def tests_skipping_enabled?
@tests_skipping_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY)
end

def flaky_test_retries_enabled?
return @flaky_test_retries_enabled if defined?(@flaky_test_retries_enabled)

@flaky_test_retries_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY)
end

private

def bool(key)
Expand Down
9 changes: 8 additions & 1 deletion lib/datadog/ci/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,14 @@ def parameters
private

def record_test_result(datadog_status)
test_suite&.record_test_result(datadog_status)
test_id = Utils::TestRun.skippable_test_id(name, test_suite_name, parameters)

# if this test was already executed in this test suite, mark it as retried
if test_suite&.test_executed?(test_id)
set_tag(Ext::Test::TAG_IS_RETRY, "true")
end

test_suite&.record_test_result(test_id, datadog_status)
end
end
end
Expand Down
28 changes: 28 additions & 0 deletions lib/datadog/ci/test_retries/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Datadog
module CI
module TestRetries
# 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
class Component
attr_reader :retry_failed_tests_enabled, :retry_failed_tests_max_attempts, :retry_failed_tests_total_limit

def initialize(
retry_failed_tests_max_attempts:,
retry_failed_tests_total_limit:
)
# enabled only by remote settings
@retry_failed_tests_enabled = false
@retry_failed_tests_max_attempts = retry_failed_tests_max_attempts
@retry_failed_tests_total_limit = retry_failed_tests_total_limit
end

def configure(library_settings)
@retry_failed_tests_enabled = library_settings.flaky_test_retries_enabled?
end
end
end
end
end
Loading

0 comments on commit 2eb656d

Please sign in to comment.