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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -2
  3. data/lib/datadog/ci/configuration/components.rb +12 -6
  4. data/lib/datadog/ci/configuration/settings.rb +6 -0
  5. data/lib/datadog/ci/contrib/cucumber/filter.rb +40 -0
  6. data/lib/datadog/ci/contrib/cucumber/instrumentation.rb +15 -1
  7. data/lib/datadog/ci/contrib/cucumber/patcher.rb +0 -2
  8. data/lib/datadog/ci/contrib/minitest/runner.rb +6 -1
  9. data/lib/datadog/ci/contrib/minitest/test.rb +4 -0
  10. data/lib/datadog/ci/contrib/rspec/example.rb +11 -10
  11. data/lib/datadog/ci/contrib/rspec/runner.rb +2 -1
  12. data/lib/datadog/ci/ext/settings.rb +1 -0
  13. data/lib/datadog/ci/ext/telemetry.rb +9 -0
  14. data/lib/datadog/ci/ext/test.rb +6 -1
  15. data/lib/datadog/ci/ext/transport.rb +7 -0
  16. data/lib/datadog/ci/remote/component.rb +13 -2
  17. data/lib/datadog/ci/remote/library_settings.rb +48 -7
  18. data/lib/datadog/ci/remote/library_settings_client.rb +2 -1
  19. data/lib/datadog/ci/remote/slow_test_retries.rb +53 -0
  20. data/lib/datadog/ci/test.rb +15 -2
  21. data/lib/datadog/ci/test_optimisation/component.rb +9 -6
  22. data/lib/datadog/ci/test_optimisation/skippable.rb +1 -1
  23. data/lib/datadog/ci/test_retries/component.rb +68 -39
  24. data/lib/datadog/ci/test_retries/driver/base.rb +25 -0
  25. data/lib/datadog/ci/test_retries/driver/no_retry.rb +16 -0
  26. data/lib/datadog/ci/test_retries/driver/retry_failed.rb +37 -0
  27. data/lib/datadog/ci/test_retries/driver/retry_new.rb +50 -0
  28. data/lib/datadog/ci/test_retries/null_component.rb +7 -6
  29. data/lib/datadog/ci/test_retries/strategy/base.rb +11 -4
  30. data/lib/datadog/ci/test_retries/strategy/no_retry.rb +0 -2
  31. data/lib/datadog/ci/test_retries/strategy/retry_failed.rb +30 -13
  32. data/lib/datadog/ci/test_retries/strategy/retry_new.rb +132 -0
  33. data/lib/datadog/ci/test_retries/unique_tests_client.rb +132 -0
  34. data/lib/datadog/ci/test_session.rb +2 -0
  35. data/lib/datadog/ci/test_suite.rb +8 -0
  36. data/lib/datadog/ci/test_visibility/component.rb +23 -13
  37. data/lib/datadog/ci/test_visibility/null_component.rb +1 -1
  38. data/lib/datadog/ci/test_visibility/telemetry.rb +9 -0
  39. data/lib/datadog/ci/utils/test_run.rb +1 -1
  40. data/lib/datadog/ci/version.rb +2 -2
  41. data/lib/datadog/ci.rb +6 -3
  42. metadata +11 -5
  43. data/lib/datadog/ci/contrib/cucumber/configuration_override.rb +0 -37
  44. data/lib/datadog/ci/utils/identity.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ddc261b14aac25efcc4c94bf30e18962e714a83e35d47cb3a1f3210d640fd5a
4
- data.tar.gz: 47acdc6af95b2ab2e7385a22d0c82359e63ff0eabb8f0dfdf1bd0197f73eb6ee
3
+ metadata.gz: 876a0e7ada3aeaf87b19dade6ee73fec0fe8d37387cde5fc31b43aaad76e99c5
4
+ data.tar.gz: 8c46ad7c35a621ae03b7b42faa5b3c6934066e422ff4d3a8ac2249cd6466ecd3
5
5
  SHA512:
6
- metadata.gz: 1a774a633e3da98fc313dcf2bfeb14c02e231fc52fc7abced54ae4246afd9930fe4598d5885e39a999ede252da6c9aa0262e041aad7b4873d4fcdb8349dab673
7
- data.tar.gz: ed05b4354183b31a619f9f18aa22c547dc0ccbfdd829e8fa24e250fe98becd2a434855bb71839325214d017d6e1b79c99dcf681eeb7f820e1508991516722070
6
+ metadata.gz: 8f312e6920cad6072665c00dc5d253e66a7c3272abda20e79f86cf11beb38fffe0be789f644affb6f8920e458f00f221c7969fb84c2ee6f2198af6a140eafc64
7
+ data.tar.gz: f9b7c2b184e7a140269eaa2069cbf5062270d6bb07410fd9b46cab4c521a8d0bd694a676086164c58300c264357e53e0118611053f6bdf0c2dee757b487b4769
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.5.0] - 2024-09-18
4
+
5
+ ### Added
6
+ * Retry new tests - parse remote configuration and fetch unique known tests ([#227][])
7
+ * early flake detection support for rspec and minitest ([#229][])
8
+ * Early flake detection support for Cucumber ([#231][])
9
+
10
+ ### Fixed
11
+ * Minor telemetry fixes ([#226][])
12
+
3
13
  ## [1.4.1] - 2024-08-28
4
14
 
5
15
  ### Fixed
@@ -315,7 +325,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
315
325
 
316
326
  - Ruby versions < 2.7 no longer supported ([#8][])
317
327
 
318
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.4.1...main
328
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.5.0...main
329
+ [1.5.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.4.1...v1.5.0
319
330
  [1.4.1]: https://github.com/DataDog/datadog-ci-rb/compare/v1.4.0...v1.4.1
320
331
  [1.4.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.3.0...v1.4.0
321
332
  [1.3.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.2.0...v1.3.0
@@ -452,4 +463,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
452
463
  [#219]: https://github.com/DataDog/datadog-ci-rb/issues/219
453
464
  [#220]: https://github.com/DataDog/datadog-ci-rb/issues/220
454
465
  [#221]: https://github.com/DataDog/datadog-ci-rb/issues/221
455
- [#224]: https://github.com/DataDog/datadog-ci-rb/issues/224
466
+ [#224]: https://github.com/DataDog/datadog-ci-rb/issues/224
467
+ [#226]: https://github.com/DataDog/datadog-ci-rb/issues/226
468
+ [#227]: https://github.com/DataDog/datadog-ci-rb/issues/227
469
+ [#229]: https://github.com/DataDog/datadog-ci-rb/issues/229
470
+ [#231]: https://github.com/DataDog/datadog-ci-rb/issues/231
@@ -11,6 +11,7 @@ require_relative "../test_optimisation/coverage/transport"
11
11
  require_relative "../test_optimisation/coverage/writer"
12
12
  require_relative "../test_retries/component"
13
13
  require_relative "../test_retries/null_component"
14
+ require_relative "../test_retries/unique_tests_client"
14
15
  require_relative "../test_visibility/component"
15
16
  require_relative "../test_visibility/flush"
16
17
  require_relative "../test_visibility/null_component"
@@ -19,7 +20,6 @@ require_relative "../test_visibility/serializers/factories/test_suite_level"
19
20
  require_relative "../test_visibility/transport"
20
21
  require_relative "../transport/adapters/telemetry_webmock_safe_adapter"
21
22
  require_relative "../transport/api/builder"
22
- require_relative "../utils/identity"
23
23
  require_relative "../utils/parsing"
24
24
  require_relative "../utils/test_run"
25
25
  require_relative "../worker"
@@ -116,7 +116,9 @@ module Datadog
116
116
  @test_retries = TestRetries::Component.new(
117
117
  retry_failed_tests_enabled: settings.ci.retry_failed_tests_enabled,
118
118
  retry_failed_tests_max_attempts: settings.ci.retry_failed_tests_max_attempts,
119
- retry_failed_tests_total_limit: settings.ci.retry_failed_tests_total_limit
119
+ retry_failed_tests_total_limit: settings.ci.retry_failed_tests_total_limit,
120
+ retry_new_tests_enabled: settings.ci.retry_new_tests_enabled,
121
+ unique_tests_client: build_unique_tests_client(settings, test_visibility_api)
120
122
  )
121
123
  # @type ivar @test_optimisation: Datadog::CI::TestOptimisation::Component
122
124
  @test_optimisation = build_test_optimisation(settings, test_visibility_api)
@@ -236,6 +238,14 @@ module Datadog
236
238
  )
237
239
  end
238
240
 
241
+ def build_unique_tests_client(settings, api)
242
+ TestRetries::UniqueTestsClient.new(
243
+ api: api,
244
+ dd_env: settings.env,
245
+ config_tags: custom_configuration(settings)
246
+ )
247
+ end
248
+
239
249
  # fetch custom tags provided by the user in DD_TAGS env var
240
250
  # with prefix test.configuration.
241
251
  def custom_configuration(settings)
@@ -274,12 +284,8 @@ module Datadog
274
284
  settings.telemetry.shutdown_timeout_seconds = 60.0
275
285
 
276
286
  begin
277
- require "datadog/core/environment/identity"
278
287
  require "datadog/core/telemetry/http/adapters/net"
279
288
 
280
- # patch gem's identity to report datadog-ci library version instead of datadog gem version
281
- Core::Environment::Identity.include(CI::Utils::Identity)
282
-
283
289
  # patch gem's telemetry transport layer to use Net::HTTP instead of WebMock's Net::HTTP
284
290
  Core::Telemetry::Http::Adapters::Net.include(CI::Transport::Adapters::TelemetryWebmockSafeAdapter)
285
291
  rescue => e
@@ -106,6 +106,12 @@ module Datadog
106
106
  o.default 1000
107
107
  end
108
108
 
109
+ option :retry_new_tests_enabled do |o|
110
+ o.type :bool
111
+ o.env CI::Ext::Settings::ENV_RETRY_NEW_TESTS_ENABLED
112
+ o.default true
113
+ end
114
+
109
115
  define_method(:instrument) do |integration_name, options = {}, &block|
110
116
  return unless enabled
111
117
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Contrib
6
+ module Cucumber
7
+ class Filter < ::Cucumber::Core::Filter.new(:configuration)
8
+ def test_case(test_case)
9
+ test_retries_component.reset_retries! unless test_case_seen[test_case]
10
+ test_case_seen[test_case] = true
11
+
12
+ configuration.on_event(:test_case_finished) do |event|
13
+ next unless retry_required?(test_case, event)
14
+
15
+ test_case.describe_to(receiver)
16
+ end
17
+
18
+ super
19
+ end
20
+
21
+ private
22
+
23
+ def retry_required?(test_case, event)
24
+ return false unless event.test_case == test_case
25
+
26
+ test_retries_component.should_retry?
27
+ end
28
+
29
+ def test_case_seen
30
+ @test_case_seen ||= Hash.new { |h, k| h[k] = false }
31
+ end
32
+
33
+ def test_retries_component
34
+ @test_retries_component ||= Datadog.send(:components).test_retries
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -18,10 +18,24 @@ module Datadog
18
18
 
19
19
  def formatters
20
20
  existing_formatters = super
21
- @datadog_formatter ||= CI::Contrib::Cucumber::Formatter.new(@configuration)
21
+ @datadog_formatter ||= Formatter.new(@configuration)
22
22
  [@datadog_formatter] + existing_formatters
23
23
  end
24
24
 
25
+ def filters
26
+ require_relative "filter"
27
+
28
+ filters_list = super
29
+ datadog_filter = Filter.new(@configuration)
30
+ unless @configuration.dry_run?
31
+ # insert our filter the pre-last position because Cucumber::Filters::PrepareWorld must be the last one
32
+ # see:
33
+ # https://github.com/cucumber/cucumber-ruby/blob/58dd8f12c0ac5f4e607335ff2e7d385c1ed25899/lib/cucumber/runtime.rb#L266
34
+ filters_list.insert(-2, datadog_filter)
35
+ end
36
+ filters_list
37
+ end
38
+
25
39
  def begin_scenario(test_case)
26
40
  if Datadog::CI.active_test&.skipped_by_itr?
27
41
  raise ::Cucumber::Core::Test::Result::Skipped, CI::Ext::Test::ITR_TEST_SKIP_REASON
@@ -3,7 +3,6 @@
3
3
  require "datadog/tracing/contrib/patcher"
4
4
 
5
5
  require_relative "instrumentation"
6
- require_relative "configuration_override"
7
6
 
8
7
  module Datadog
9
8
  module CI
@@ -21,7 +20,6 @@ module Datadog
21
20
 
22
21
  def patch
23
22
  ::Cucumber::Runtime.include(Instrumentation)
24
- ::Cucumber::Configuration.include(ConfigurationOverride)
25
23
  end
26
24
  end
27
25
  end
@@ -8,6 +8,8 @@ module Datadog
8
8
  module Contrib
9
9
  module Minitest
10
10
  module Runner
11
+ DD_ESTIMATED_TESTS_PER_SUITE = 5
12
+
11
13
  def self.included(base)
12
14
  base.singleton_class.prepend(ClassMethods)
13
15
  end
@@ -18,12 +20,15 @@ module Datadog
18
20
 
19
21
  return unless datadog_configuration[:enabled]
20
22
 
23
+ # minitest does not store the total number of tests, so we can't pass it to the test session
24
+ # instead, we use the number of test suites * DD_ESTIMATED_TESTS_PER_SUITE as a rough estimate
21
25
  test_visibility_component.start_test_session(
22
26
  tags: {
23
27
  CI::Ext::Test::TAG_FRAMEWORK => Ext::FRAMEWORK,
24
28
  CI::Ext::Test::TAG_FRAMEWORK_VERSION => CI::Contrib::Minitest::Integration.version.to_s
25
29
  },
26
- service: datadog_configuration[:service_name]
30
+ service: datadog_configuration[:service_name],
31
+ total_tests_count: (DD_ESTIMATED_TESTS_PER_SUITE * ::Minitest::Runnable.runnables.size).to_i
27
32
  )
28
33
  test_visibility_component.start_test_module(Ext::FRAMEWORK)
29
34
  end
@@ -51,6 +51,10 @@ module Datadog
51
51
  return super unless test_span
52
52
 
53
53
  finish_with_result(test_span, result_code)
54
+
55
+ # remove failures if test passed at least once on retries
56
+ self.failures = [] if test_span.any_retry_passed?
57
+
54
58
  if Helpers.parallel?(self.class)
55
59
  finish_with_result(test_span.test_suite, result_code)
56
60
  end
@@ -64,9 +64,9 @@ module Datadog
64
64
 
65
65
  result = super
66
66
 
67
- # In case when test job is canceled and RSpec is quitting we don't want to report the last test
67
+ # When test job is canceled and RSpec is quitting we don't want to report the last test
68
68
  # before RSpec context unwinds. This test might have some unrelated errors that we don't want to
69
- # report.
69
+ # see in Datadog.
70
70
  return result if ::RSpec.world.wants_to_quit
71
71
 
72
72
  case execution_result.status
@@ -74,6 +74,8 @@ module Datadog
74
74
  test_span&.passed!
75
75
  when :failed
76
76
  test_span&.failed!(exception: execution_result.exception)
77
+ # if any of the retries passed, we don't fail the test run
78
+ @exception = nil if test_span&.any_retry_passed?
77
79
  else
78
80
  # :pending or nil
79
81
  test_span&.skipped!(
@@ -84,21 +86,20 @@ module Datadog
84
86
  end
85
87
  end
86
88
 
87
- # after retries are done, we can report the test to RSpec
88
- @skip_reporting = false
89
-
90
89
  # this is a special case for ci-queue, we need to finish the test suite span
91
90
  ci_queue_test_span&.finish
92
91
 
93
- # Finish spec with latest retry's result
94
- # TODO: when implementing new test retries make sure to clean @exception before calling this method
95
- # if test passed at least once
92
+ # after retries are done, we can finally report the test to RSpec
93
+ @skip_reporting = false
96
94
  finish(reporter)
97
95
  end
98
96
 
99
97
  def finish(reporter)
100
- # by default finish test but do not report it to RSpec::Core::Reporter
101
- # it is going to be reported once after retries are done
98
+ # By default finish test but do not report it to RSpec::Core::Reporter
99
+ # it is going to be reported once after retries are done.
100
+ #
101
+ # We need to do this because RSpec breaks when we try to report the same example multiple times with different
102
+ # results.
102
103
  return super unless @skip_reporting
103
104
 
104
105
  super(::RSpec::Core::NullReporter)
@@ -23,7 +23,8 @@ module Datadog
23
23
  CI::Ext::Test::TAG_FRAMEWORK => Ext::FRAMEWORK,
24
24
  CI::Ext::Test::TAG_FRAMEWORK_VERSION => CI::Contrib::RSpec::Integration.version.to_s
25
25
  },
26
- service: datadog_configuration[:service_name]
26
+ service: datadog_configuration[:service_name],
27
+ total_tests_count: ::RSpec.world.example_count
27
28
  )
28
29
 
29
30
  test_module = test_visibility_component.start_test_module(Ext::FRAMEWORK)
@@ -18,6 +18,7 @@ module Datadog
18
18
  ENV_RETRY_FAILED_TESTS_ENABLED = "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED"
19
19
  ENV_RETRY_FAILED_TESTS_MAX_ATTEMPTS = "DD_CIVISIBILITY_FLAKY_RETRY_COUNT"
20
20
  ENV_RETRY_FAILED_TESTS_TOTAL_LIMIT = "DD_CIVISIBILITY_TOTAL_FLAKY_RETRY_COUNT"
21
+ ENV_RETRY_NEW_TESTS_ENABLED = "DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED"
21
22
 
22
23
  # Source: https://docs.datadoghq.com/getting_started/site/
23
24
  DD_SITE_ALLOWLIST = %w[
@@ -54,6 +54,12 @@ module Datadog
54
54
  METRIC_CODE_COVERAGE_IS_EMPTY = "code_coverage.is_empty"
55
55
  METRIC_CODE_COVERAGE_FILES = "code_coverage.files"
56
56
 
57
+ METRIC_EFD_UNIQUE_TESTS_REQUEST = "early_flake_detection.request"
58
+ METRIC_EFD_UNIQUE_TESTS_REQUEST_MS = "early_flake_detection.request_ms"
59
+ METRIC_EFD_UNIQUE_TESTS_REQUEST_ERRORS = "early_flake_detection.request_errors"
60
+ METRIC_EFD_UNIQUE_TESTS_RESPONSE_BYTES = "early_flake_detection.response_bytes"
61
+ METRIC_EFD_UNIQUE_TESTS_RESPONSE_TESTS = "early_flake_detection.response_tests"
62
+
57
63
  METRIC_TEST_SESSION = "test_session"
58
64
 
59
65
  TAG_TEST_FRAMEWORK = "test_framework"
@@ -63,6 +69,7 @@ module Datadog
63
69
  TAG_BROWSER_DRIVER = "browser_driver"
64
70
  TAG_IS_RUM = "is_rum"
65
71
  TAG_IS_RETRY = "is_retry"
72
+ TAG_IS_NEW = "is_new"
66
73
  TAG_LIBRARY = "library"
67
74
  TAG_ENDPOINT = "endpoint"
68
75
  TAG_ERROR_TYPE = "error_type"
@@ -73,6 +80,8 @@ module Datadog
73
80
  TAG_COMMAND = "command"
74
81
  TAG_COVERAGE_ENABLED = "coverage_enabled"
75
82
  TAG_ITR_SKIP_ENABLED = "itrskip_enabled"
83
+ TAG_EARLY_FLAKE_DETECTION_ENABLED = "early_flake_detection_enabled"
84
+ TAG_EARLY_FLAKE_DETECTION_ABORT_REASON = "early_flake_detection_abort_reason"
76
85
  TAG_PROVIDER = "provider"
77
86
  TAG_AUTO_INJECTED = "auto_injected"
78
87
 
@@ -58,8 +58,11 @@ module Datadog
58
58
  # version of the browser, if multiple browsers or multiple versions then this tag is empty
59
59
  TAG_BROWSER_VERSION = "test.browser.version"
60
60
 
61
- # Tags for test retries
61
+ # Tags for retries
62
62
  TAG_IS_RETRY = "test.is_retry" # true if test was retried by datadog-ci library
63
+ TAG_IS_NEW = "test.is_new" # true if test was marked as new by new test retries (early flake detection)
64
+ TAG_EARLY_FLAKE_ENABLED = "test.early_flake.enabled" # true if early flake detection is enabled
65
+ TAG_EARLY_FLAKE_ABORT_REASON = "test.early_flake.abort_reason" # reason why early flake detection was aborted
63
66
 
64
67
  # internal APM tag to mark a span as a test span
65
68
  TAG_SPAN_KIND = "span.kind"
@@ -73,6 +76,8 @@ module Datadog
73
76
  ITR_TEST_SKIP_REASON = "Skipped by Datadog's intelligent test runner"
74
77
  ITR_UNSKIPPABLE_OPTION = :datadog_itr_unskippable
75
78
 
79
+ EARLY_FLAKE_FAULTY = "faulty"
80
+
76
81
  # test status as recognized by Datadog
77
82
  module Status
78
83
  PASS = "pass"
@@ -37,6 +37,10 @@ module Datadog
37
37
  DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY = "tests_skipping"
38
38
  DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY = "require_git"
39
39
  DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY = "flaky_test_retries_enabled"
40
+ DD_API_SETTINGS_RESPONSE_EARLY_FLAKE_DETECTION_KEY = "early_flake_detection"
41
+ DD_API_SETTINGS_RESPONSE_ENABLED_KEY = "enabled"
42
+ DD_API_SETTINGS_RESPONSE_SLOW_TEST_RETRIES_KEY = "slow_test_retries"
43
+ DD_API_SETTINGS_RESPONSE_FAULTY_SESSION_THRESHOLD_KEY = "faulty_session_threshold"
40
44
  DD_API_SETTINGS_RESPONSE_DEFAULT = {DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY => false}.freeze
41
45
 
42
46
  DD_API_GIT_SEARCH_COMMITS_PATH = "/api/v2/git/repository/search_commits"
@@ -46,6 +50,9 @@ module Datadog
46
50
  DD_API_SKIPPABLE_TESTS_PATH = "/api/v2/ci/tests/skippable"
47
51
  DD_API_SKIPPABLE_TESTS_TYPE = "test_params"
48
52
 
53
+ DD_API_UNIQUE_TESTS_PATH = "/api/v2/ci/libraries/tests"
54
+ DD_API_UNIQUE_TESTS_TYPE = "ci_app_libraries_tests_request"
55
+
49
56
  CONTENT_TYPE_MESSAGEPACK = "application/msgpack"
50
57
  CONTENT_TYPE_JSON = "application/json"
51
58
  CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../worker"
4
+
3
5
  module Datadog
4
6
  module CI
5
7
  module Remote
@@ -27,8 +29,17 @@ module Datadog
27
29
  end
28
30
  end
29
31
 
30
- test_optimisation.configure(library_configuration, test_session)
31
- test_retries.configure(library_configuration)
32
+ # configure different components in parallel because they might block on HTTP requests
33
+ configuration_workers = [
34
+ Worker.new { test_optimisation.configure(library_configuration, test_session) },
35
+ Worker.new { test_retries.configure(library_configuration, test_session) }
36
+ ]
37
+
38
+ # launch configuration workers
39
+ configuration_workers.each(&:perform)
40
+
41
+ # block until all workers are done (or 60 seconds has passed)
42
+ configuration_workers.each(&:wait_until_done)
32
43
  end
33
44
 
34
45
  private
@@ -7,6 +7,8 @@ require_relative "../ext/transport"
7
7
  require_relative "../transport/telemetry"
8
8
  require_relative "../utils/parsing"
9
9
 
10
+ require_relative "slow_test_retries"
11
+
10
12
  module Datadog
11
13
  module CI
12
14
  module Remote
@@ -49,37 +51,76 @@ module Datadog
49
51
  def require_git?
50
52
  return @require_git if defined?(@require_git)
51
53
 
52
- @require_git = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY)
54
+ @require_git = Utils::Parsing.convert_to_bool(
55
+ payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY, false)
56
+ )
53
57
  end
54
58
 
55
59
  def itr_enabled?
56
60
  return @itr_enabled if defined?(@itr_enabled)
57
61
 
58
- @itr_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY)
62
+ @itr_enabled = Utils::Parsing.convert_to_bool(
63
+ payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY, false)
64
+ )
59
65
  end
60
66
 
61
67
  def code_coverage_enabled?
62
68
  return @code_coverage_enabled if defined?(@code_coverage_enabled)
63
69
 
64
- @code_coverage_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY)
70
+ @code_coverage_enabled = Utils::Parsing.convert_to_bool(
71
+ payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY, false)
72
+ )
65
73
  end
66
74
 
67
75
  def tests_skipping_enabled?
68
76
  return @tests_skipping_enabled if defined?(@tests_skipping_enabled)
69
77
 
70
- @tests_skipping_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY)
78
+ @tests_skipping_enabled = Utils::Parsing.convert_to_bool(
79
+ payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY, false)
80
+ )
71
81
  end
72
82
 
73
83
  def flaky_test_retries_enabled?
74
84
  return @flaky_test_retries_enabled if defined?(@flaky_test_retries_enabled)
75
85
 
76
- @flaky_test_retries_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY)
86
+ @flaky_test_retries_enabled = Utils::Parsing.convert_to_bool(
87
+ payload.fetch(
88
+ Ext::Transport::DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY, false
89
+ )
90
+ )
91
+ end
92
+
93
+ def early_flake_detection_enabled?
94
+ return @early_flake_detection_enabled if defined?(@early_flake_detection_enabled)
95
+
96
+ @early_flake_detection_enabled = Utils::Parsing.convert_to_bool(
97
+ early_flake_detection_payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_ENABLED_KEY, false)
98
+ )
99
+ end
100
+
101
+ def slow_test_retries
102
+ return @slow_test_retries if defined?(@slow_test_retries)
103
+
104
+ @slow_test_retries = SlowTestRetries.new(
105
+ early_flake_detection_payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_SLOW_TEST_RETRIES_KEY, {})
106
+ )
107
+ end
108
+
109
+ def faulty_session_threshold
110
+ return @faulty_session_threshold if defined?(@faulty_session_threshold)
111
+
112
+ @faulty_session_threshold = early_flake_detection_payload.fetch(
113
+ Ext::Transport::DD_API_SETTINGS_RESPONSE_FAULTY_SESSION_THRESHOLD_KEY, 0
114
+ )
77
115
  end
78
116
 
79
117
  private
80
118
 
81
- def bool(key)
82
- Utils::Parsing.convert_to_bool(payload.fetch(key, false))
119
+ def early_flake_detection_payload
120
+ payload.fetch(
121
+ Ext::Transport::DD_API_SETTINGS_RESPONSE_EARLY_FLAKE_DETECTION_KEY,
122
+ {}
123
+ )
83
124
  end
84
125
 
85
126
  def default_payload
@@ -58,7 +58,8 @@ module Datadog
58
58
  1,
59
59
  {
60
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
61
+ Ext::Telemetry::TAG_ITR_SKIP_ENABLED => library_settings.tests_skipping_enabled?.to_s,
62
+ Ext::Telemetry::TAG_EARLY_FLAKE_DETECTION_ENABLED => library_settings.early_flake_detection_enabled?.to_s
62
63
  }
63
64
  )
64
65
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Remote
6
+ # Parses "slow_test_retries" payload for early flake detection settings
7
+ #
8
+ # Example payload:
9
+ # {
10
+ # "5s" => 10,
11
+ # "10s" => 5,
12
+ # "30s" => 3,
13
+ # "5m" => 2
14
+ # }
15
+ #
16
+ # The payload above means that for tests that run less than 5 seconds, we should retry them 10 times,
17
+ # for tests that run less than 10 seconds, we should retry them 5 times, and so on.
18
+ class SlowTestRetries
19
+ attr_reader :entries
20
+
21
+ Entry = Struct.new(:duration, :max_attempts)
22
+
23
+ DURATION_MEASURES = {
24
+ "s" => 1,
25
+ "m" => 60
26
+ }.freeze
27
+
28
+ def initialize(payload)
29
+ @entries = parse(payload)
30
+ end
31
+
32
+ def max_attempts_for_duration(duration)
33
+ @entries.each do |entry|
34
+ return entry.max_attempts if duration < entry.duration
35
+ end
36
+
37
+ 0
38
+ end
39
+
40
+ private
41
+
42
+ def parse(payload)
43
+ (payload || {}).keys.filter_map do |key|
44
+ duration, measure = key.match(/(\d+)(\w+)/)&.captures
45
+ next if duration.nil? || measure.nil? || !DURATION_MEASURES.key?(measure)
46
+
47
+ Entry.new(duration.to_f * DURATION_MEASURES.fetch(measure, 1), payload[key].to_i)
48
+ end.sort_by(&:duration)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -70,6 +70,12 @@ module Datadog
70
70
  get_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR) == "true"
71
71
  end
72
72
 
73
+ # Returns "true" if test span represents a retry.
74
+ # @return [Boolean] true if this test is a retry, false otherwise.
75
+ def is_retry?
76
+ get_tag(Ext::Test::TAG_IS_RETRY) == "true"
77
+ end
78
+
73
79
  # Marks this test as unskippable by the intelligent test runner.
74
80
  # This must be done before the test execution starts.
75
81
  #
@@ -139,11 +145,18 @@ module Datadog
139
145
  get_tag(Ext::Test::TAG_PARAMETERS)
140
146
  end
141
147
 
148
+ # @internal
149
+ def any_retry_passed?
150
+ !!test_suite&.any_test_retry_passed?(test_id)
151
+ end
152
+
142
153
  private
143
154
 
144
- def record_test_result(datadog_status)
145
- test_id = Utils::TestRun.skippable_test_id(name, test_suite_name, parameters)
155
+ def test_id
156
+ @test_id ||= Utils::TestRun.datadog_test_id(name, test_suite_name, parameters)
157
+ end
146
158
 
159
+ def record_test_result(datadog_status)
147
160
  # if this test was already executed in this test suite, mark it as retried
148
161
  if test_suite&.test_executed?(test_id)
149
162
  set_tag(Ext::Test::TAG_IS_RETRY, "true")
@@ -99,6 +99,7 @@ module Datadog
99
99
 
100
100
  def start_coverage(test)
101
101
  return if !enabled? || !code_coverage?
102
+
102
103
  Telemetry.code_coverage_started(test)
103
104
  coverage_collector&.start
104
105
  end
@@ -109,13 +110,15 @@ module Datadog
109
110
  Telemetry.code_coverage_finished(test)
110
111
 
111
112
  coverage = coverage_collector&.stop
113
+
114
+ # if test was skipped, we discard coverage data
115
+ return if test.skipped?
116
+
112
117
  if coverage.nil? || coverage.empty?
113
118
  Telemetry.code_coverage_is_empty
114
119
  return
115
120
  end
116
121
 
117
- return if test.skipped?
118
-
119
122
  test_source_file = test.source_file
120
123
 
121
124
  # cucumber's gherkin files are not covered by the code coverage collector
@@ -140,8 +143,8 @@ module Datadog
140
143
  def mark_if_skippable(test)
141
144
  return if !enabled? || !skipping_tests?
142
145
 
143
- skippable_test_id = Utils::TestRun.skippable_test_id(test.name, test.test_suite_name, test.parameters)
144
- if @skippable_tests.include?(skippable_test_id)
146
+ datadog_test_id = Utils::TestRun.datadog_test_id(test.name, test.test_suite_name, test.parameters)
147
+ if @skippable_tests.include?(datadog_test_id)
145
148
  if forked?
146
149
  Datadog.logger.warn { "Intelligent test runner is not supported for forking test runners yet" }
147
150
  return
@@ -149,9 +152,9 @@ module Datadog
149
152
 
150
153
  test.set_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR, "true")
151
154
 
152
- Datadog.logger.debug { "Marked test as skippable: #{skippable_test_id}" }
155
+ Datadog.logger.debug { "Marked test as skippable: #{datadog_test_id}" }
153
156
  else
154
- Datadog.logger.debug { "Test is not skippable: #{skippable_test_id}" }
157
+ Datadog.logger.debug { "Test is not skippable: #{datadog_test_id}" }
155
158
  end
156
159
  end
157
160
 
@@ -36,7 +36,7 @@ module Datadog
36
36
  next unless test_data["type"] == Ext::Test::ITR_TEST_SKIPPING_MODE
37
37
 
38
38
  attrs = test_data["attributes"] || {}
39
- res << Utils::TestRun.skippable_test_id(attrs["name"], attrs["suite"], attrs["parameters"])
39
+ res << Utils::TestRun.datadog_test_id(attrs["name"], attrs["suite"], attrs["parameters"])
40
40
  end
41
41
 
42
42
  res