datadog-ci 1.13.0 → 1.15.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -2
  3. data/lib/datadog/ci/configuration/components.rb +2 -1
  4. data/lib/datadog/ci/configuration/settings.rb +6 -0
  5. data/lib/datadog/ci/contrib/minitest/helpers.rb +26 -0
  6. data/lib/datadog/ci/contrib/minitest/runnable.rb +1 -19
  7. data/lib/datadog/ci/contrib/minitest/runner.rb +12 -5
  8. data/lib/datadog/ci/contrib/minitest/test.rb +2 -9
  9. data/lib/datadog/ci/contrib/parallel_tests/cli.rb +84 -0
  10. data/lib/datadog/ci/contrib/parallel_tests/configuration/settings.rb +32 -0
  11. data/lib/datadog/ci/contrib/parallel_tests/ext.rb +16 -0
  12. data/lib/datadog/ci/contrib/parallel_tests/integration.rb +42 -0
  13. data/lib/datadog/ci/contrib/parallel_tests/patcher.rb +24 -0
  14. data/lib/datadog/ci/contrib/rspec/example.rb +7 -0
  15. data/lib/datadog/ci/contrib/rspec/example_group.rb +18 -8
  16. data/lib/datadog/ci/contrib/rspec/helpers.rb +18 -0
  17. data/lib/datadog/ci/contrib/rspec/runner.rb +3 -1
  18. data/lib/datadog/ci/ext/settings.rb +1 -0
  19. data/lib/datadog/ci/ext/test.rb +20 -0
  20. data/lib/datadog/ci/git/local_repository.rb +1 -1
  21. data/lib/datadog/ci/git/tree_uploader.rb +9 -0
  22. data/lib/datadog/ci/readonly_test_module.rb +28 -0
  23. data/lib/datadog/ci/readonly_test_session.rb +31 -0
  24. data/lib/datadog/ci/remote/component.rb +43 -16
  25. data/lib/datadog/ci/span.rb +4 -0
  26. data/lib/datadog/ci/test_management/component.rb +34 -1
  27. data/lib/datadog/ci/test_management/tests_properties.rb +2 -1
  28. data/lib/datadog/ci/test_optimisation/component.rb +38 -33
  29. data/lib/datadog/ci/test_optimisation/skippable_percentage/base.rb +4 -0
  30. data/lib/datadog/ci/test_optimisation/skippable_percentage/calculator.rb +3 -3
  31. data/lib/datadog/ci/test_retries/strategy/retry_new.rb +1 -1
  32. data/lib/datadog/ci/test_session.rb +7 -1
  33. data/lib/datadog/ci/test_suite.rb +18 -0
  34. data/lib/datadog/ci/test_visibility/component.rb +130 -30
  35. data/lib/datadog/ci/test_visibility/context.rb +102 -41
  36. data/lib/datadog/ci/test_visibility/null_component.rb +5 -1
  37. data/lib/datadog/ci/test_visibility/store/{local.rb → fiber_local.rb} +1 -1
  38. data/lib/datadog/ci/test_visibility/store/{global.rb → process.rb} +26 -14
  39. data/lib/datadog/ci/test_visibility/transport.rb +1 -2
  40. data/lib/datadog/ci/transport/http.rb +1 -1
  41. data/lib/datadog/ci/utils/file_storage.rb +57 -0
  42. data/lib/datadog/ci/utils/stateful.rb +52 -0
  43. data/lib/datadog/ci/version.rb +1 -1
  44. data/lib/datadog/ci.rb +5 -4
  45. metadata +28 -5
  46. data/lib/datadog/ci/test_visibility/capabilities.rb +0 -36
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../worker"
4
+ require_relative "../utils/stateful"
4
5
 
5
6
  module Datadog
6
7
  module CI
@@ -8,33 +9,44 @@ module Datadog
8
9
  # Remote configuration component.
9
10
  # Responsible for fetching library settings and configuring the library accordingly.
10
11
  class Component
12
+ include Datadog::CI::Utils::Stateful
13
+
14
+ FILE_STORAGE_KEY = "remote_component_state"
15
+
11
16
  def initialize(library_settings_client:)
12
17
  @library_settings_client = library_settings_client
18
+ @library_configuration = nil
13
19
  end
14
20
 
15
21
  # called on test session start, uses test session info to send configuration request to the backend
16
22
  def configure(test_session)
17
- library_configuration = @library_settings_client.fetch(test_session)
18
- # sometimes we can skip code coverage for default branch if there are no changes in the repository
19
- # backend needs git metadata uploaded for this test session to check if we can skip code coverage
20
- if library_configuration.require_git?
21
- Datadog.logger.debug { "Library configuration endpoint requires git upload to be finished, waiting..." }
22
- git_tree_upload_worker.wait_until_done
23
-
24
- Datadog.logger.debug { "Requesting library configuration again..." }
25
- library_configuration = @library_settings_client.fetch(test_session)
26
-
27
- if library_configuration.require_git?
28
- Datadog.logger.debug { "git metadata upload did not complete in time when configuring library" }
23
+ # If component state is loaded successfully, skip fetching library configuration
24
+ unless load_component_state
25
+ @library_configuration = @library_settings_client.fetch(test_session)
26
+ # sometimes we can skip code coverage for default branch if there are no changes in the repository
27
+ # backend needs git metadata uploaded for this test session to check if we can skip code coverage
28
+ if @library_configuration.require_git?
29
+ Datadog.logger.debug { "Library configuration endpoint requires git upload to be finished, waiting..." }
30
+ git_tree_upload_worker.wait_until_done
31
+
32
+ Datadog.logger.debug { "Requesting library configuration again..." }
33
+ @library_configuration = @library_settings_client.fetch(test_session)
34
+
35
+ if @library_configuration.require_git?
36
+ Datadog.logger.debug { "git metadata upload did not complete in time when configuring library" }
37
+ end
29
38
  end
39
+
40
+ # Store component state for distributed test runs
41
+ store_component_state if test_session.distributed
30
42
  end
31
43
 
32
44
  # configure different components in parallel because they might block on HTTP requests
33
45
  configuration_workers = [
34
- Worker.new { test_optimisation.configure(library_configuration, test_session) },
35
- Worker.new { test_retries.configure(library_configuration, test_session) },
36
- Worker.new { test_visibility.configure(library_configuration, test_session) },
37
- Worker.new { test_management.configure(library_configuration, test_session) }
46
+ Worker.new { test_optimisation.configure(@library_configuration, test_session) },
47
+ Worker.new { test_retries.configure(@library_configuration, test_session) },
48
+ Worker.new { test_visibility.configure(@library_configuration, test_session) },
49
+ Worker.new { test_management.configure(@library_configuration, test_session) }
38
50
  ]
39
51
 
40
52
  # launch configuration workers
@@ -44,6 +56,21 @@ module Datadog
44
56
  configuration_workers.each(&:wait_until_done)
45
57
  end
46
58
 
59
+ # Implementation of Stateful interface
60
+ def serialize_state
61
+ {
62
+ library_configuration: @library_configuration
63
+ }
64
+ end
65
+
66
+ def restore_state(state)
67
+ @library_configuration = state[:library_configuration]
68
+ end
69
+
70
+ def storage_key
71
+ FILE_STORAGE_KEY
72
+ end
73
+
47
74
  private
48
75
 
49
76
  def test_management
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "drb"
4
+
3
5
  require "datadog/core/environment/platform"
4
6
 
5
7
  require_relative "ext/test"
@@ -12,6 +14,8 @@ module Datadog
12
14
  #
13
15
  # @public_api
14
16
  class Span
17
+ include DRb::DRbUndumped
18
+
15
19
  attr_reader :tracer_span
16
20
 
17
21
  def initialize(tracer_span)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../ext/telemetry"
4
4
  require_relative "../ext/test"
5
+ require_relative "../utils/stateful"
5
6
  require_relative "../utils/telemetry"
6
7
  require_relative "../utils/test_run"
7
8
 
@@ -14,6 +15,10 @@ module Datadog
14
15
  # - marking test as disabled causes test to be skipped
15
16
  # - marking test as "attempted to fix" causes test to be retried many times to confirm that fix worked
16
17
  class Component
18
+ include Datadog::CI::Utils::Stateful
19
+
20
+ FILE_STORAGE_KEY = "test_management_component_state"
21
+
17
22
  attr_reader :enabled, :tests_properties
18
23
 
19
24
  def initialize(enabled:, tests_properties_client:)
@@ -30,7 +35,11 @@ module Datadog
30
35
 
31
36
  test_session.set_tag(Ext::Test::TAG_TEST_MANAGEMENT_ENABLED, "true")
32
37
 
33
- @tests_properties = @tests_properties_client.fetch(test_session)
38
+ # Load component state first, and if successful, skip fetching tests properties
39
+ if !load_component_state
40
+ @tests_properties = @tests_properties_client.fetch(test_session)
41
+ store_component_state if test_session.distributed
42
+ end
34
43
 
35
44
  Utils::Telemetry.distribution(
36
45
  Ext::Telemetry::METRIC_TEST_MANAGEMENT_TESTS_RESPONSE_TESTS,
@@ -55,6 +64,30 @@ module Datadog
55
64
  test_span.set_tag(Ext::Test::TAG_IS_TEST_DISABLED, "true") if test_properties["disabled"]
56
65
  test_span.set_tag(Ext::Test::TAG_IS_ATTEMPT_TO_FIX, "true") if test_properties["attempt_to_fix"]
57
66
  end
67
+
68
+ def attempt_to_fix?(datadog_fqn_test_id)
69
+ return false unless @enabled
70
+
71
+ test_properties = @tests_properties[datadog_fqn_test_id]
72
+ return false if test_properties.nil?
73
+
74
+ test_properties.fetch("attempt_to_fix", false)
75
+ end
76
+
77
+ # Implementation of Stateful interface
78
+ def serialize_state
79
+ {
80
+ tests_properties: @tests_properties
81
+ }
82
+ end
83
+
84
+ def restore_state(state)
85
+ @tests_properties = state[:tests_properties]
86
+ end
87
+
88
+ def storage_key
89
+ FILE_STORAGE_KEY
90
+ end
58
91
  end
59
92
  end
60
93
  end
@@ -117,7 +117,8 @@ module Datadog
117
117
  "id" => Datadog::Core::Environment::Identity.id,
118
118
  "type" => Ext::Transport::DD_API_TEST_MANAGEMENT_TESTS_TYPE,
119
119
  "attributes" => {
120
- "repository_url" => test_session.git_repository_url
120
+ "repository_url" => test_session.git_repository_url,
121
+ "commit_message" => test_session.git_commit_message
121
122
  }
122
123
  }
123
124
  }.to_json
@@ -3,7 +3,6 @@
3
3
  require "pp"
4
4
 
5
5
  require "datadog/core/telemetry/logging"
6
- require "datadog/core/utils/forking"
7
6
 
8
7
  require_relative "../ext/test"
9
8
  require_relative "../ext/telemetry"
@@ -11,6 +10,7 @@ require_relative "../ext/telemetry"
11
10
  require_relative "../git/local_repository"
12
11
 
13
12
  require_relative "../utils/parsing"
13
+ require_relative "../utils/stateful"
14
14
  require_relative "../utils/telemetry"
15
15
 
16
16
  require_relative "coverage/event"
@@ -24,10 +24,11 @@ module Datadog
24
24
  # Integrates with backend to provide test impact analysis data and
25
25
  # skip tests that are not impacted by the changes
26
26
  class Component
27
- include Core::Utils::Forking
27
+ include Datadog::CI::Utils::Stateful
28
+
29
+ FILE_STORAGE_KEY = "test_optimisation_component_state"
28
30
 
29
31
  attr_reader :correlation_id, :skippable_tests, :skippable_tests_fetch_error,
30
- :skipped_tests_count, :total_tests_count,
31
32
  :enabled, :test_skipping_enabled, :code_coverage_enabled
32
33
 
33
34
  def initialize(
@@ -61,9 +62,6 @@ module Datadog
61
62
  @correlation_id = nil
62
63
  @skippable_tests = Set.new
63
64
 
64
- @total_tests_count = 0
65
- @skipped_tests_count = 0
66
-
67
65
  @mutex = Mutex.new
68
66
 
69
67
  Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
@@ -85,9 +83,13 @@ module Datadog
85
83
 
86
84
  load_datadog_cov! if @code_coverage_enabled
87
85
 
88
- Datadog.logger.debug("Configured TestOptimisation with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}")
86
+ # Load component state first, and if successful, skip fetching skippable tests
87
+ if skipping_tests? && !load_component_state
88
+ fetch_skippable_tests(test_session)
89
+ store_component_state if test_session.distributed
90
+ end
89
91
 
90
- fetch_skippable_tests(test_session)
92
+ Datadog.logger.debug("Configured TestOptimisation with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}")
91
93
  end
92
94
 
93
95
  def enabled?
@@ -145,21 +147,16 @@ module Datadog
145
147
  event
146
148
  end
147
149
 
148
- def skippable?(test)
150
+ def skippable?(datadog_test_id)
149
151
  return false if !enabled? || !skipping_tests?
150
152
 
151
- @skippable_tests.include?(test.datadog_test_id)
153
+ @skippable_tests.include?(datadog_test_id)
152
154
  end
153
155
 
154
156
  def mark_if_skippable(test)
155
157
  return if !enabled? || !skipping_tests?
156
158
 
157
- if skippable?(test)
158
- if forked?
159
- Datadog.logger.warn { "Test Impact Analysis is not supported for forking test runners yet" }
160
- return
161
- end
162
-
159
+ if skippable?(test.datadog_test_id) && !test.attempt_to_fix?
163
160
  test.set_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR, "true")
164
161
 
165
162
  Datadog.logger.debug { "Marked test as skippable: #{test.datadog_test_id}" }
@@ -168,31 +165,22 @@ module Datadog
168
165
  end
169
166
  end
170
167
 
171
- def count_skipped_test(test)
172
- @mutex.synchronize do
173
- @total_tests_count += 1
174
-
175
- return if !test.skipped? || !test.skipped_by_test_impact_analysis?
176
-
177
- if forked?
178
- Datadog.logger.warn { "ITR is not supported for forking test runners yet" }
179
- return
180
- end
168
+ def on_test_finished(test, context)
169
+ return if !test.skipped? || !test.skipped_by_test_impact_analysis?
181
170
 
182
- Telemetry.itr_skipped
171
+ Telemetry.itr_skipped
183
172
 
184
- @skipped_tests_count += 1
185
- end
173
+ context.incr_tests_skipped_by_tia_count
186
174
  end
187
175
 
188
- def write_test_session_tags(test_session)
176
+ def write_test_session_tags(test_session, skipped_tests_count)
189
177
  return if !enabled?
190
178
 
191
179
  Datadog.logger.debug { "Finished optimised session with test skipping enabled: #{@test_skipping_enabled}" }
192
- Datadog.logger.debug { "#{@skipped_tests_count} tests were skipped" }
180
+ Datadog.logger.debug { "#{skipped_tests_count} tests were skipped" }
193
181
 
194
- test_session.set_tag(Ext::Test::TAG_ITR_TESTS_SKIPPED, @skipped_tests_count.positive?.to_s)
195
- test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_COUNT, @skipped_tests_count)
182
+ test_session.set_tag(Ext::Test::TAG_ITR_TESTS_SKIPPED, skipped_tests_count.positive?.to_s)
183
+ test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_COUNT, skipped_tests_count)
196
184
  end
197
185
 
198
186
  def skippable_tests_count
@@ -203,6 +191,23 @@ module Datadog
203
191
  @coverage_writer&.stop
204
192
  end
205
193
 
194
+ # Implementation of Stateful interface
195
+ def serialize_state
196
+ {
197
+ correlation_id: @correlation_id,
198
+ skippable_tests: @skippable_tests
199
+ }
200
+ end
201
+
202
+ def restore_state(state)
203
+ @correlation_id = state[:correlation_id]
204
+ @skippable_tests = state[:skippable_tests]
205
+ end
206
+
207
+ def storage_key
208
+ FILE_STORAGE_KEY
209
+ end
210
+
206
211
  private
207
212
 
208
213
  def write(event)
@@ -44,6 +44,10 @@ module Datadog
44
44
  def test_optimisation
45
45
  Datadog.send(:components).test_optimisation
46
46
  end
47
+
48
+ def test_visibility
49
+ Datadog.send(:components).test_visibility
50
+ end
47
51
  end
48
52
  end
49
53
  end
@@ -34,11 +34,11 @@ module Datadog
34
34
  return 0.0
35
35
  end
36
36
 
37
- log("Total tests count: #{test_optimisation.total_tests_count}")
38
- log("Skipped tests count: #{test_optimisation.skipped_tests_count}")
37
+ log("Total tests count: #{test_visibility.total_tests_count}")
38
+ log("Skipped tests count: #{test_visibility.tests_skipped_by_tia_count}")
39
39
  validate_test_optimisation_state!
40
40
 
41
- (test_optimisation.skipped_tests_count.to_f / test_optimisation.total_tests_count.to_f).floor(2)
41
+ (test_visibility.tests_skipped_by_tia_count.to_f / test_visibility.total_tests_count.to_f).floor(2)
42
42
  end
43
43
 
44
44
  private
@@ -70,7 +70,7 @@ module Datadog
70
70
 
71
71
  def calculate_total_retries_limit(library_settings, test_session)
72
72
  percentage_limit = library_settings.faulty_session_threshold
73
- tests_count = test_session.total_tests_count.to_i
73
+ tests_count = test_session.estimated_total_tests_count.to_i
74
74
  if tests_count.zero?
75
75
  Datadog.logger.debug do
76
76
  "Total tests count is zero, using default value for the total number of tests: [#{DEFAULT_TOTAL_TESTS_COUNT}]"
@@ -12,7 +12,7 @@ module Datadog
12
12
  #
13
13
  # @public_api
14
14
  class TestSession < ConcurrentSpan
15
- attr_accessor :total_tests_count
15
+ attr_accessor :estimated_total_tests_count, :distributed
16
16
 
17
17
  # Finishes the current test session.
18
18
  # @return [void]
@@ -46,6 +46,12 @@ module Datadog
46
46
  get_tag(Ext::Environment::TAG_JOB_NAME)
47
47
  end
48
48
 
49
+ # Returns the git commit message extracted from the environment.
50
+ # @return [String] the commit message.
51
+ def git_commit_message
52
+ get_tag(Ext::Git::TAG_COMMIT_MESSAGE)
53
+ end
54
+
49
55
  def skipping_tests?
50
56
  get_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED) == "true"
51
57
  end
@@ -84,6 +84,24 @@ module Datadog
84
84
  end
85
85
  end
86
86
 
87
+ # @internal
88
+ def set_expected_tests!(expected_tests)
89
+ synchronize do
90
+ return if @expected_tests_set
91
+
92
+ @expected_tests_set = Set.new(expected_tests)
93
+ end
94
+ end
95
+
96
+ # @internal
97
+ def expected_test_done!(test_name)
98
+ synchronize do
99
+ @expected_tests_set.delete(test_name)
100
+
101
+ finish if @expected_tests_set.empty?
102
+ end
103
+ end
104
+
87
105
  private
88
106
 
89
107
  def set_status_from_stats!