datadog-ci 1.3.0 → 1.4.1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -2
  3. data/README.md +1 -0
  4. data/ext/datadog_cov/datadog_cov.c +1 -1
  5. data/lib/datadog/ci/configuration/components.rb +25 -11
  6. data/lib/datadog/ci/configuration/settings.rb +19 -1
  7. data/lib/datadog/ci/contrib/cucumber/configuration_override.rb +37 -0
  8. data/lib/datadog/ci/contrib/cucumber/formatter.rb +5 -5
  9. data/lib/datadog/ci/contrib/cucumber/instrumentation.rb +9 -1
  10. data/lib/datadog/ci/contrib/cucumber/patcher.rb +3 -3
  11. data/lib/datadog/ci/contrib/minitest/runner.rb +16 -0
  12. data/lib/datadog/ci/contrib/minitest/test.rb +1 -0
  13. data/lib/datadog/ci/contrib/rspec/example.rb +67 -39
  14. data/lib/datadog/ci/contrib/rspec/example_group.rb +1 -1
  15. data/lib/datadog/ci/ext/settings.rb +3 -0
  16. data/lib/datadog/ci/ext/telemetry.rb +1 -0
  17. data/lib/datadog/ci/ext/test.rb +11 -11
  18. data/lib/datadog/ci/ext/transport.rb +1 -0
  19. data/lib/datadog/ci/git/local_repository.rb +32 -13
  20. data/lib/datadog/ci/remote/component.rb +50 -0
  21. data/lib/datadog/ci/remote/library_settings.rb +91 -0
  22. data/lib/datadog/ci/{transport/remote_settings_api.rb → remote/library_settings_client.rb} +11 -56
  23. data/lib/datadog/ci/test.rb +8 -1
  24. data/lib/datadog/ci/test_optimisation/component.rb +12 -16
  25. data/lib/datadog/ci/test_retries/component.rb +84 -0
  26. data/lib/datadog/ci/test_retries/null_component.rb +28 -0
  27. data/lib/datadog/ci/test_retries/strategy/base.rb +19 -0
  28. data/lib/datadog/ci/test_retries/strategy/no_retry.rb +16 -0
  29. data/lib/datadog/ci/test_retries/strategy/retry_failed.rb +37 -0
  30. data/lib/datadog/ci/test_suite.rb +39 -18
  31. data/lib/datadog/ci/test_visibility/component.rb +45 -47
  32. data/lib/datadog/ci/test_visibility/null_component.rb +6 -0
  33. data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
  34. data/lib/datadog/ci/version.rb +2 -2
  35. metadata +13 -6
  36. data/lib/datadog/ci/contrib/cucumber/step.rb +0 -27
@@ -238,23 +238,42 @@ module Datadog
238
238
  Telemetry.git_command(Ext::Telemetry::Command::UNSHALLOW)
239
239
  res = nil
240
240
 
241
+ unshallow_command =
242
+ "git fetch " \
243
+ "--shallow-since=\"1 month ago\" " \
244
+ "--update-shallow " \
245
+ "--filter=\"blob:none\" " \
246
+ "--recurse-submodules=no " \
247
+ "$(git config --default origin --get clone.defaultRemoteName)"
248
+
249
+ unshallow_remotes = [
250
+ "$(git rev-parse HEAD)",
251
+ "$(git rev-parse --abbrev-ref --symbolic-full-name @{upstream})",
252
+ nil
253
+ ]
254
+
241
255
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
242
- res = exec_git_command(
243
- "git fetch " \
244
- "--shallow-since=\"1 month ago\" " \
245
- "--update-shallow " \
246
- "--filter=\"blob:none\" " \
247
- "--recurse-submodules=no " \
248
- "$(git config --default origin --get clone.defaultRemoteName) $(git rev-parse HEAD)"
249
- )
256
+ unshallow_remotes.each do |remote|
257
+ unshallowing_errored = false
258
+
259
+ res =
260
+ begin
261
+ exec_git_command(
262
+ "#{unshallow_command} #{remote}"
263
+ )
264
+ rescue => e
265
+ log_failure(e, "git unshallow")
266
+ telemetry_track_error(e, Ext::Telemetry::Command::UNSHALLOW)
267
+ unshallowing_errored = true
268
+ nil
269
+ end
270
+
271
+ break unless unshallowing_errored
272
+ end
250
273
  end
251
- Telemetry.git_command_ms(Ext::Telemetry::Command::UNSHALLOW, duration_ms)
252
274
 
275
+ Telemetry.git_command_ms(Ext::Telemetry::Command::UNSHALLOW, duration_ms)
253
276
  res
254
- rescue => e
255
- log_failure(e, "git unshallow")
256
- telemetry_track_error(e, Ext::Telemetry::Command::UNSHALLOW)
257
- nil
258
277
  end
259
278
 
260
279
  # makes .exec_git_command private to make sure that this method
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Remote
6
+ # Remote configuration component.
7
+ # Responsible for fetching library settings and configuring the library accordingly.
8
+ class Component
9
+ def initialize(library_settings_client:)
10
+ @library_settings_client = library_settings_client
11
+ end
12
+
13
+ # called on test session start, uses test session info to send configuration request to the backend
14
+ def configure(test_session)
15
+ library_configuration = @library_settings_client.fetch(test_session)
16
+ # sometimes we can skip code coverage for default branch if there are no changes in the repository
17
+ # backend needs git metadata uploaded for this test session to check if we can skip code coverage
18
+ if library_configuration.require_git?
19
+ Datadog.logger.debug { "Library configuration endpoint requires git upload to be finished, waiting..." }
20
+ git_tree_upload_worker.wait_until_done
21
+
22
+ Datadog.logger.debug { "Requesting library configuration again..." }
23
+ library_configuration = @library_settings_client.fetch(test_session)
24
+
25
+ if library_configuration.require_git?
26
+ Datadog.logger.debug { "git metadata upload did not complete in time when configuring library" }
27
+ end
28
+ end
29
+
30
+ test_optimisation.configure(library_configuration, test_session)
31
+ test_retries.configure(library_configuration)
32
+ end
33
+
34
+ private
35
+
36
+ def test_optimisation
37
+ Datadog.send(:components).test_optimisation
38
+ end
39
+
40
+ def test_retries
41
+ Datadog.send(:components).test_retries
42
+ end
43
+
44
+ def git_tree_upload_worker
45
+ Datadog.send(:components).git_tree_upload_worker
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,91 @@
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/parsing"
9
+
10
+ module Datadog
11
+ module CI
12
+ module Remote
13
+ # Wrapper around the settings HTTP response
14
+ class LibrarySettings
15
+ def initialize(http_response)
16
+ @http_response = http_response
17
+ @json = nil
18
+ end
19
+
20
+ def ok?
21
+ resp = @http_response
22
+ !resp.nil? && resp.ok?
23
+ end
24
+
25
+ def payload
26
+ cached = @json
27
+ return cached unless cached.nil?
28
+
29
+ resp = @http_response
30
+ return @json = default_payload if resp.nil? || !ok?
31
+
32
+ begin
33
+ @json = JSON.parse(resp.payload).dig(*Ext::Transport::DD_API_SETTINGS_RESPONSE_DIG_KEYS) ||
34
+ default_payload
35
+ rescue JSON::ParserError => e
36
+ Datadog.logger.error("Failed to parse settings response payload: #{e}. Payload was: #{resp.payload}")
37
+
38
+ Transport::Telemetry.api_requests_errors(
39
+ Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_ERRORS,
40
+ 1,
41
+ error_type: "invalid_json",
42
+ status_code: nil
43
+ )
44
+
45
+ @json = default_payload
46
+ end
47
+ end
48
+
49
+ def require_git?
50
+ return @require_git if defined?(@require_git)
51
+
52
+ @require_git = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY)
53
+ end
54
+
55
+ def itr_enabled?
56
+ return @itr_enabled if defined?(@itr_enabled)
57
+
58
+ @itr_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY)
59
+ end
60
+
61
+ def code_coverage_enabled?
62
+ return @code_coverage_enabled if defined?(@code_coverage_enabled)
63
+
64
+ @code_coverage_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY)
65
+ end
66
+
67
+ def tests_skipping_enabled?
68
+ return @tests_skipping_enabled if defined?(@tests_skipping_enabled)
69
+
70
+ @tests_skipping_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY)
71
+ end
72
+
73
+ def flaky_test_retries_enabled?
74
+ return @flaky_test_retries_enabled if defined?(@flaky_test_retries_enabled)
75
+
76
+ @flaky_test_retries_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_FLAKY_TEST_RETRIES_KEY)
77
+ end
78
+
79
+ private
80
+
81
+ def bool(key)
82
+ Utils::Parsing.convert_to_bool(payload.fetch(key, false))
83
+ end
84
+
85
+ def default_payload
86
+ Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -4,73 +4,28 @@ require "json"
4
4
 
5
5
  require "datadog/core/environment/identity"
6
6
 
7
+ require_relative "library_settings"
8
+
9
+ require_relative "../ext/test"
7
10
  require_relative "../ext/telemetry"
8
11
  require_relative "../ext/transport"
9
12
  require_relative "../transport/telemetry"
10
- require_relative "../utils/parsing"
11
13
  require_relative "../utils/telemetry"
12
14
 
13
15
  module Datadog
14
16
  module CI
15
- module Transport
16
- # Datadog API client
17
+ module Remote
17
18
  # Calls settings endpoint to fetch library settings for given service and env
18
- class RemoteSettingsApi
19
- class Response
20
- def initialize(http_response)
21
- @http_response = http_response
22
- @json = nil
23
- end
24
-
25
- def ok?
26
- resp = @http_response
27
- !resp.nil? && resp.ok?
28
- end
29
-
30
- def payload
31
- cached = @json
32
- return cached unless cached.nil?
33
-
34
- resp = @http_response
35
- return @json = default_payload if resp.nil? || !ok?
36
-
37
- begin
38
- @json = JSON.parse(resp.payload).dig(*Ext::Transport::DD_API_SETTINGS_RESPONSE_DIG_KEYS) ||
39
- default_payload
40
- rescue JSON::ParserError => e
41
- Datadog.logger.error("Failed to parse settings response payload: #{e}. Payload was: #{resp.payload}")
42
-
43
- Transport::Telemetry.api_requests_errors(
44
- Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_ERRORS,
45
- 1,
46
- error_type: "invalid_json",
47
- status_code: nil
48
- )
49
-
50
- @json = default_payload
51
- end
52
- end
53
-
54
- def require_git?
55
- Utils::Parsing.convert_to_bool(payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY])
56
- end
57
-
58
- private
59
-
60
- def default_payload
61
- Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT
62
- end
63
- end
64
-
19
+ class LibrarySettingsClient
65
20
  def initialize(dd_env:, api: nil, config_tags: {})
66
21
  @api = api
67
22
  @dd_env = dd_env
68
23
  @config_tags = config_tags || {}
69
24
  end
70
25
 
71
- def fetch_library_settings(test_session)
26
+ def fetch(test_session)
72
27
  api = @api
73
- return Response.new(nil) unless api
28
+ return LibrarySettings.new(nil) unless api
74
29
 
75
30
  request_payload = payload(test_session)
76
31
  Datadog.logger.debug("Fetching library settings with request: #{request_payload}")
@@ -96,18 +51,18 @@ module Datadog
96
51
  )
97
52
  end
98
53
 
99
- response = Response.new(http_response)
54
+ library_settings = LibrarySettings.new(http_response)
100
55
 
101
56
  Utils::Telemetry.inc(
102
57
  Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_RESPONSE,
103
58
  1,
104
59
  {
105
- Ext::Telemetry::TAG_COVERAGE_ENABLED => response.payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY],
106
- Ext::Telemetry::TAG_ITR_SKIP_ENABLED => response.payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY]
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
107
62
  }
108
63
  )
109
64
 
110
- response
65
+ library_settings
111
66
  end
112
67
 
113
68
  private
@@ -142,7 +142,14 @@ module Datadog
142
142
  private
143
143
 
144
144
  def record_test_result(datadog_status)
145
- test_suite&.record_test_result(datadog_status)
145
+ test_id = Utils::TestRun.skippable_test_id(name, test_suite_name, parameters)
146
+
147
+ # if this test was already executed in this test suite, mark it as retried
148
+ if test_suite&.test_executed?(test_id)
149
+ set_tag(Ext::Test::TAG_IS_RETRY, "true")
150
+ end
151
+
152
+ test_suite&.record_test_result(test_id, datadog_status)
146
153
  end
147
154
  end
148
155
  end
@@ -6,7 +6,6 @@ require "datadog/core/utils/forking"
6
6
 
7
7
  require_relative "../ext/test"
8
8
  require_relative "../ext/telemetry"
9
- require_relative "../ext/transport"
10
9
 
11
10
  require_relative "../git/local_repository"
12
11
 
@@ -65,24 +64,17 @@ module Datadog
65
64
  Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
66
65
  end
67
66
 
68
- def configure(remote_configuration, test_session:, git_tree_upload_worker:)
67
+ def configure(remote_configuration, test_session)
68
+ return unless enabled?
69
+
69
70
  Datadog.logger.debug("Configuring TestOptimisation with remote configuration: #{remote_configuration}")
70
71
 
71
- @enabled = Utils::Parsing.convert_to_bool(
72
- remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY, false)
73
- )
74
- @test_skipping_enabled = @enabled && Utils::Parsing.convert_to_bool(
75
- remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY, false)
76
- )
77
- @code_coverage_enabled = @enabled && Utils::Parsing.convert_to_bool(
78
- remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY, false)
79
- )
72
+ @enabled = remote_configuration.itr_enabled?
73
+ @test_skipping_enabled = @enabled && remote_configuration.tests_skipping_enabled?
74
+ @code_coverage_enabled = @enabled && remote_configuration.code_coverage_enabled?
80
75
 
81
76
  test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED, @test_skipping_enabled)
82
- # currently we set this tag when ITR requires collecting code coverage
83
- # this will change as soon as we implement total code coverage support in this library
84
77
  test_session.set_tag(Ext::Test::TAG_CODE_COVERAGE_ENABLED, @code_coverage_enabled)
85
-
86
78
  # we skip tests, not suites
87
79
  test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, Ext::Test::ITR_TEST_SKIPPING_MODE)
88
80
 
@@ -90,7 +82,7 @@ module Datadog
90
82
 
91
83
  Datadog.logger.debug("Configured TestOptimisation with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}")
92
84
 
93
- fetch_skippable_tests(test_session: test_session, git_tree_upload_worker: git_tree_upload_worker)
85
+ fetch_skippable_tests(test_session)
94
86
  end
95
87
 
96
88
  def enabled?
@@ -225,7 +217,7 @@ module Datadog
225
217
  coverage[absolute_test_source_file_path] = true
226
218
  end
227
219
 
228
- def fetch_skippable_tests(test_session:, git_tree_upload_worker:)
220
+ def fetch_skippable_tests(test_session)
229
221
  return unless skipping_tests?
230
222
 
231
223
  # we can only request skippable tests if git metadata is already uploaded
@@ -248,6 +240,10 @@ module Datadog
248
240
  def code_coverage_mode
249
241
  @use_single_threaded_coverage ? :single : :multi
250
242
  end
243
+
244
+ def git_tree_upload_worker
245
+ Datadog.send(:components).git_tree_upload_worker
246
+ end
251
247
  end
252
248
  end
253
249
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "strategy/no_retry"
4
+ require_relative "strategy/retry_failed"
5
+
6
+ module Datadog
7
+ module CI
8
+ module TestRetries
9
+ # Encapsulates the logic to enable test retries, including:
10
+ # - retrying failed tests - improve success rate of CI pipelines
11
+ # - retrying new tests - detect flaky tests as early as possible to prevent them from being merged
12
+ class Component
13
+ attr_reader :retry_failed_tests_enabled, :retry_failed_tests_max_attempts,
14
+ :retry_failed_tests_total_limit, :retry_failed_tests_count
15
+
16
+ def initialize(
17
+ retry_failed_tests_enabled:,
18
+ retry_failed_tests_max_attempts:,
19
+ retry_failed_tests_total_limit:
20
+ )
21
+ @retry_failed_tests_enabled = retry_failed_tests_enabled
22
+ @retry_failed_tests_max_attempts = retry_failed_tests_max_attempts
23
+ @retry_failed_tests_total_limit = retry_failed_tests_total_limit
24
+ # counter that store the current number of failed tests retried
25
+ @retry_failed_tests_count = 0
26
+
27
+ @mutex = Mutex.new
28
+ end
29
+
30
+ def configure(library_settings)
31
+ @retry_failed_tests_enabled &&= library_settings.flaky_test_retries_enabled?
32
+ end
33
+
34
+ def with_retries(&block)
35
+ # @type var retry_strategy: Strategy::Base
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)
49
+
50
+ loop do
51
+ yield
52
+
53
+ break unless retry_strategy&.should_retry?
54
+ end
55
+ ensure
56
+ test_visibility_component.remove_test_finished_callback
57
+ end
58
+
59
+ def build_strategy(test_span)
60
+ @mutex.synchronize do
61
+ if should_retry_failed_test?(test_span)
62
+ Datadog.logger.debug("Failed test retry starts")
63
+ @retry_failed_tests_count += 1
64
+
65
+ Strategy::RetryFailed.new(max_attempts: @retry_failed_tests_max_attempts)
66
+ else
67
+ Strategy::NoRetry.new
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def should_retry_failed_test?(test_span)
75
+ @retry_failed_tests_enabled && !!test_span&.failed? && @retry_failed_tests_count < @retry_failed_tests_total_limit
76
+ end
77
+
78
+ def test_visibility_component
79
+ Datadog.send(:components).test_visibility
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "component"
4
+
5
+ module Datadog
6
+ module CI
7
+ module TestRetries
8
+ class NullComponent < Component
9
+ attr_reader :retry_failed_tests_enabled, :retry_failed_tests_max_attempts, :retry_failed_tests_total_limit
10
+
11
+ 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
+ end
17
+
18
+ def configure(library_settings)
19
+ end
20
+
21
+ def with_retries(&block)
22
+ no_action = proc {}
23
+ yield no_action
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module TestRetries
6
+ module Strategy
7
+ class Base
8
+ def should_retry?
9
+ false
10
+ end
11
+
12
+ def record_retry(test_span)
13
+ test_span&.set_tag(Ext::Test::TAG_IS_RETRY, "true")
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Datadog
6
+ module CI
7
+ module TestRetries
8
+ module Strategy
9
+ class NoRetry < Base
10
+ def record_retry(test_span)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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 Strategy
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
@@ -16,7 +16,9 @@ module Datadog
16
16
  def initialize(tracer_span)
17
17
  super
18
18
 
19
- @test_suite_stats = Hash.new(0)
19
+ # counts how many times every test in this suite was executed with each status:
20
+ # { "MySuite.mytest.a:1" => { "pass" => 3, "fail" => 2 } }
21
+ @execution_stats_per_test = {}
20
22
  end
21
23
 
22
24
  # Finishes this test suite.
@@ -33,42 +35,61 @@ module Datadog
33
35
  end
34
36
 
35
37
  # @internal
36
- def record_test_result(datadog_test_status)
38
+ def record_test_result(test_id, datadog_test_status)
37
39
  synchronize do
38
- @test_suite_stats[datadog_test_status] += 1
40
+ @execution_stats_per_test[test_id] ||= Hash.new(0)
41
+ @execution_stats_per_test[test_id][datadog_test_status] += 1
39
42
  end
40
43
  end
41
44
 
42
45
  # @internal
43
- def passed_tests_count
46
+ def any_passed?
44
47
  synchronize do
45
- @test_suite_stats[Ext::Test::Status::PASS]
48
+ @execution_stats_per_test.any? do |_, stats|
49
+ stats[Ext::Test::Status::PASS] > 0
50
+ end
46
51
  end
47
52
  end
48
53
 
49
54
  # @internal
50
- def skipped_tests_count
55
+ def test_executed?(test_id)
51
56
  synchronize do
52
- @test_suite_stats[Ext::Test::Status::SKIP]
57
+ @execution_stats_per_test.key?(test_id)
53
58
  end
54
59
  end
55
60
 
56
- # @internal
57
- def failed_tests_count
61
+ private
62
+
63
+ def set_status_from_stats!
58
64
  synchronize do
59
- @test_suite_stats[Ext::Test::Status::FAIL]
65
+ # count how many tests passed, failed and skipped
66
+ test_suite_stats = @execution_stats_per_test.each_with_object(Hash.new(0)) do |(_test_id, stats), acc|
67
+ acc[derive_test_status_from_execution_stats(stats)] += 1
68
+ end
69
+
70
+ # test suite is considered failed if at least one test failed
71
+ if test_suite_stats[Ext::Test::Status::FAIL] > 0
72
+ failed!
73
+ # if there are no failures and no passes, it is skipped
74
+ elsif test_suite_stats[Ext::Test::Status::PASS] == 0
75
+ skipped!
76
+ # otherwise we consider it passed
77
+ else
78
+ passed!
79
+ end
60
80
  end
61
81
  end
62
82
 
63
- private
64
-
65
- def set_status_from_stats!
66
- if failed_tests_count > 0
67
- failed!
68
- elsif passed_tests_count == 0
69
- skipped!
83
+ def derive_test_status_from_execution_stats(test_execution_stats)
84
+ # test is passed if it passed at least once
85
+ if test_execution_stats[Ext::Test::Status::PASS] > 0
86
+ Ext::Test::Status::PASS
87
+ # if test was never passed, it is failed if it failed at least once
88
+ elsif test_execution_stats[Ext::Test::Status::FAIL] > 0
89
+ Ext::Test::Status::FAIL
90
+ # otherwise it is skipped
70
91
  else
71
- passed!
92
+ Ext::Test::Status::SKIP
72
93
  end
73
94
  end
74
95
  end