datadog-ci 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4cb94bf0ef94f54289e38a26649d24eeaaca99ba91229f5ebb096c046bdc582
4
- data.tar.gz: 9a88847a58121a1516b95fc1abac1fa89d4a1bab5c4593f932fc804410ee8a57
3
+ metadata.gz: cd2dfb59d1dd3d17b2e0ccea1f03308fe38b65ce6daeb3f903d158fc0b4ed47b
4
+ data.tar.gz: cc1a87dcd6fdab402556df3dd0f8a042a4dc96438c4aa7796091ff71a49435b0
5
5
  SHA512:
6
- metadata.gz: 7830a7b52821efbea8e0f86e878588ef96718e845fd7a33575a4a5e92d350e26e36bb4b78f7ce78b58ff59654c62973c4eca58f21c63b958d44f4d8feff122cc
7
- data.tar.gz: da8d30e84f9dea71eb498cccb6e03cb092e27e773008dc7533188020ee5cb59de774e4d2be32821e3fd82e28f4ccb426572695fd532dc7f7fff6479a392bca85
6
+ metadata.gz: d7eacb805c74e1c627c3f0494fab65c45b5bf940cbd124b9c027edd0eb4c21fcaaadd052aa5e7a1e60f2bc9d38b6e6787676a9f64e2fe93e234af4e8d3370117
7
+ data.tar.gz: 4d10fc9811a302f609fa36be74b0cbd81d4ae690cdeeb1f333025b7ffbdd398ef92bc6eb8ee455f2d34a9bc83ff76b06cbe091c3d4fe697ded4ce8f9ebf5f75d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.8.0] - 2024-10-17
4
+
5
+ ### Added
6
+ * Add command line tool to compute a percentage of skippable tests for RSpec ([#194][])
7
+
8
+ ### Changed
9
+ * Bump gem datadog dependency to 2.4 and update test dependencies ([#248][])
10
+ * Optimise LocalRepository.relative_to_root helper to make test impact analysis faster ([#244][])
11
+ * Retry HTTP requests on 429 and 5xx responses ([#243][])
12
+ * Use correct monotonic clock time if Timecop.mock_process_clock is set ([#242][])
13
+
3
14
  ## [1.7.0] - 2024-09-25
4
15
 
5
16
  ### Added
@@ -339,7 +350,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
339
350
 
340
351
  - Ruby versions < 2.7 no longer supported ([#8][])
341
352
 
342
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.7.0...main
353
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.8.0...main
354
+ [1.8.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.7.0...v1.8.0
343
355
  [1.7.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.6.0...v1.7.0
344
356
  [1.6.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.5.0...v1.6.0
345
357
  [1.5.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.4.1...v1.5.0
@@ -459,6 +471,7 @@ Currently test suite level visibility is not used by our instrumentation: it wil
459
471
  [#189]: https://github.com/DataDog/datadog-ci-rb/issues/189
460
472
  [#190]: https://github.com/DataDog/datadog-ci-rb/issues/190
461
473
  [#193]: https://github.com/DataDog/datadog-ci-rb/issues/193
474
+ [#194]: https://github.com/DataDog/datadog-ci-rb/issues/194
462
475
  [#197]: https://github.com/DataDog/datadog-ci-rb/issues/197
463
476
  [#200]: https://github.com/DataDog/datadog-ci-rb/issues/200
464
477
  [#201]: https://github.com/DataDog/datadog-ci-rb/issues/201
@@ -488,4 +501,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
488
501
  [#236]: https://github.com/DataDog/datadog-ci-rb/issues/236
489
502
  [#238]: https://github.com/DataDog/datadog-ci-rb/issues/238
490
503
  [#239]: https://github.com/DataDog/datadog-ci-rb/issues/239
491
- [#240]: https://github.com/DataDog/datadog-ci-rb/issues/240
504
+ [#240]: https://github.com/DataDog/datadog-ci-rb/issues/240
505
+ [#242]: https://github.com/DataDog/datadog-ci-rb/issues/242
506
+ [#243]: https://github.com/DataDog/datadog-ci-rb/issues/243
507
+ [#244]: https://github.com/DataDog/datadog-ci-rb/issues/244
508
+ [#248]: https://github.com/DataDog/datadog-ci-rb/issues/248
data/exe/ddcirb ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "datadog/ci/cli/cli"
4
+
5
+ Datadog::CI::CLI.exec(ARGV.first)
@@ -0,0 +1,24 @@
1
+ require "datadog"
2
+ require "datadog/ci"
3
+
4
+ require_relative "command/skippable_tests_percentage"
5
+ require_relative "command/skippable_tests_percentage_estimate"
6
+
7
+ module Datadog
8
+ module CI
9
+ module CLI
10
+ def self.exec(action)
11
+ case action
12
+ when "skipped-tests", "skippable-tests"
13
+ Command::SkippableTestsPercentage.new.exec
14
+ when "skipped-tests-estimate", "skippable-tests-estimate"
15
+ Command::SkippableTestsPercentageEstimate.new.exec
16
+ else
17
+ puts("Usage: bundle exec ddcirb [command] [options]. Available commands:")
18
+ puts(" skippable-tests - calculates the exact percentage of skipped tests and prints it to stdout or file")
19
+ puts(" skippable-tests-estimate - estimates the percentage of skipped tests and prints it to stdout or file")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ require "optparse"
2
+
3
+ module Datadog
4
+ module CI
5
+ module CLI
6
+ module Command
7
+ class Base
8
+ def exec
9
+ action = build_action
10
+ result = action&.call
11
+
12
+ validate!(action)
13
+ output(result)
14
+ end
15
+
16
+ private
17
+
18
+ def build_action
19
+ end
20
+
21
+ def options
22
+ return @options if defined?(@options)
23
+
24
+ ddcirb_options = {}
25
+ OptionParser.new do |opts|
26
+ opts.banner = "Usage: bundle exec ddcirb [command] [options]\n Available commands: skippable-tests, skippable-tests-estimate"
27
+
28
+ opts.on("-f", "--file FILENAME", "Output result to file FILENAME")
29
+ opts.on("--verbose", "Verbose output to stdout")
30
+
31
+ command_options(opts)
32
+ end.parse!(into: ddcirb_options)
33
+
34
+ @options = ddcirb_options
35
+ end
36
+
37
+ def command_options(opts)
38
+ end
39
+
40
+ def validate!(action)
41
+ if action.nil? || action.failed
42
+ Datadog.logger.error("ddcirb failed, exiting")
43
+ Kernel.exit(1)
44
+ end
45
+ end
46
+
47
+ def output(result)
48
+ if options[:file]
49
+ File.write(options[:file], result)
50
+ else
51
+ print(result)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "base"
2
+ require_relative "../../test_optimisation/skippable_percentage/calculator"
3
+
4
+ module Datadog
5
+ module CI
6
+ module CLI
7
+ module Command
8
+ class SkippableTestsPercentage < Base
9
+ private
10
+
11
+ def build_action
12
+ ::Datadog::CI::TestOptimisation::SkippablePercentage::Calculator.new(
13
+ rspec_cli_options: (options[:"rspec-opts"] || "").split,
14
+ verbose: !options[:verbose].nil?,
15
+ spec_path: options[:"spec-path"] || "spec"
16
+ )
17
+ end
18
+
19
+ def command_options(opts)
20
+ opts.on("--rspec-opts=[OPTIONS]", "Command line options to pass to RSpec")
21
+ opts.on("--spec-path=[SPEC_PATH]", "Relative path to the spec directory, example: spec")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "base"
2
+ require_relative "../../test_optimisation/skippable_percentage/estimator"
3
+
4
+ module Datadog
5
+ module CI
6
+ module CLI
7
+ module Command
8
+ class SkippableTestsPercentageEstimate < Base
9
+ private
10
+
11
+ def build_action
12
+ ::Datadog::CI::TestOptimisation::SkippablePercentage::Estimator.new(
13
+ verbose: !options[:verbose].nil?,
14
+ spec_path: options[:"spec-path"] || "spec"
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -19,6 +19,7 @@ require_relative "../test_visibility/serializers/factories/test_level"
19
19
  require_relative "../test_visibility/serializers/factories/test_suite_level"
20
20
  require_relative "../test_visibility/transport"
21
21
  require_relative "../transport/adapters/telemetry_webmock_safe_adapter"
22
+ require_relative "../test_visibility/null_transport"
22
23
  require_relative "../transport/api/builder"
23
24
  require_relative "../utils/parsing"
24
25
  require_relative "../utils/test_run"
@@ -79,16 +80,8 @@ module Datadog
79
80
  # startup logs are useless for test visibility and create noise
80
81
  settings.diagnostics.startup_logs.enabled = false
81
82
 
82
- # When timecop is present, Time.now is mocked and .now_without_mock_time is added on Time to
83
- # get the current time without the mock.
84
- if timecop?
85
- settings.time_now_provider = -> do
86
- Time.now_without_mock_time
87
- rescue NoMethodError
88
- # fallback to normal Time.now if Time.now_without_mock_time is not defined for any reason
89
- Time.now
90
- end
91
- end
83
+ # timecop configuration
84
+ configure_time_providers(settings)
92
85
 
93
86
  # Configure Datadog::Tracing module
94
87
 
@@ -181,7 +174,6 @@ module Datadog
181
174
  # Tests are running without CI visibility enabled
182
175
  settings.ci.enabled = false
183
176
  end
184
-
185
177
  else
186
178
  Datadog.logger.debug("CI visibility configured to use agent transport via EVP proxy")
187
179
 
@@ -203,6 +195,9 @@ module Datadog
203
195
  end
204
196
 
205
197
  def build_tracing_transport(settings, api)
198
+ # NullTransport ignores traces
199
+ return TestVisibility::NullTransport.new if settings.ci.discard_traces
200
+ # nil means that default legacy APM transport will be used (only for very old Datadog Agent versions)
206
201
  return nil if api.nil?
207
202
 
208
203
  TestVisibility::Transport.new(
@@ -213,7 +208,8 @@ module Datadog
213
208
  end
214
209
 
215
210
  def build_coverage_writer(settings, api)
216
- return nil if api.nil?
211
+ # nil means that coverage event will be ignored
212
+ return nil if api.nil? || settings.ci.discard_traces
217
213
 
218
214
  TestOptimisation::Coverage::Writer.new(
219
215
  transport: TestOptimisation::Coverage::Transport.new(api: api)
@@ -294,6 +290,29 @@ module Datadog
294
290
  end
295
291
  end
296
292
 
293
+ # When timecop is present:
294
+ # - Time.now is mocked and .now_without_mock_time is added on Time to get the current time without the mock.
295
+ # - Process.clock_gettime is mocked and .clock_gettime_without_mock is added on Process to get the monotonic time without the mock.
296
+ def configure_time_providers(settings)
297
+ return unless timecop?
298
+
299
+ settings.time_now_provider = -> do
300
+ Time.now_without_mock_time
301
+ rescue NoMethodError
302
+ # fallback to normal Time.now if Time.now_without_mock_time is not defined for any reason
303
+ Time.now
304
+ end
305
+
306
+ if defined?(Process.clock_gettime_without_mock)
307
+ settings.get_time_provider = ->(unit = :float_second) do
308
+ ::Process.clock_gettime_without_mock(::Process::CLOCK_MONOTONIC, unit)
309
+ rescue NoMethodError
310
+ # fallback to normal Process.clock_gettime if Process.clock_gettime_without_mock is not defined for any reason
311
+ Process.clock_gettime(::Process::CLOCK_MONOTONIC, unit)
312
+ end
313
+ end
314
+ end
315
+
297
316
  def timecop?
298
317
  Gem.loaded_specs.key?("timecop") || !!defined?(Timecop)
299
318
  end
@@ -117,6 +117,12 @@ module Datadog
117
117
  o.default true
118
118
  end
119
119
 
120
+ # internal only
121
+ option :discard_traces do |o|
122
+ o.type :bool
123
+ o.default false
124
+ end
125
+
120
126
  define_method(:instrument) do |integration_name, options = {}, &block|
121
127
  return unless enabled
122
128
 
@@ -24,6 +24,12 @@ module Datadog
24
24
  Utils::Configuration.fetch_service_name(Ext::DEFAULT_SERVICE_NAME)
25
25
  end
26
26
  end
27
+
28
+ # internal only
29
+ option :dry_run_enabled do |o|
30
+ o.type :bool
31
+ o.default false
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -17,7 +17,7 @@ module Datadog
17
17
 
18
18
  module InstanceMethods
19
19
  def run(*args)
20
- return super if ::RSpec.configuration.dry_run?
20
+ return super if ::RSpec.configuration.dry_run? && !datadog_configuration[:dry_run_enabled]
21
21
  return super unless datadog_configuration[:enabled]
22
22
 
23
23
  test_name = full_description.strip
@@ -16,7 +16,7 @@ module Datadog
16
16
  # Instance methods for configuration
17
17
  module ClassMethods
18
18
  def run(reporter = ::RSpec::Core::NullReporter)
19
- return super if ::RSpec.configuration.dry_run?
19
+ return super if ::RSpec.configuration.dry_run? && !datadog_configuration[:dry_run_enabled]
20
20
  return super unless datadog_configuration[:enabled]
21
21
  return super unless top_level?
22
22
 
@@ -15,7 +15,7 @@ module Datadog
15
15
 
16
16
  module InstanceMethods
17
17
  def knapsack__run_specs(*args)
18
- return super if ::RSpec.configuration.dry_run?
18
+ return super if ::RSpec.configuration.dry_run? && !datadog_configuration[:dry_run_enabled]
19
19
  return super unless datadog_configuration[:enabled]
20
20
 
21
21
  test_session = test_visibility_component.start_test_session(
@@ -15,7 +15,7 @@ module Datadog
15
15
 
16
16
  module InstanceMethods
17
17
  def run_specs(*args)
18
- return super if ::RSpec.configuration.dry_run?
18
+ return super if ::RSpec.configuration.dry_run? && !datadog_configuration[:dry_run_enabled]
19
19
  return super unless datadog_configuration[:enabled]
20
20
 
21
21
  test_session = test_visibility_component.start_test_session(
@@ -12,6 +12,7 @@ module Datadog
12
12
  HEADER_CONTENT_ENCODING = "Content-Encoding"
13
13
  HEADER_EVP_SUBDOMAIN = "X-Datadog-EVP-Subdomain"
14
14
  HEADER_CONTAINER_ID = "Datadog-Container-ID"
15
+ HEADER_RATELIMIT_RESET = "X-RateLimit-Reset"
15
16
 
16
17
  EVP_PROXY_V2_PATH_PREFIX = "/evp_proxy/v2/"
17
18
  EVP_PROXY_V4_PATH_PREFIX = "/evp_proxy/v4/"
@@ -30,16 +30,44 @@ module Datadog
30
30
  @root = git_root || Dir.pwd
31
31
  end
32
32
 
33
+ # ATTENTION: this function is running in a hot path
34
+ # and should be optimized for performance
33
35
  def self.relative_to_root(path)
34
36
  return "" if path.nil?
35
37
 
36
38
  root_path = root
37
39
  return path if root_path.nil?
38
40
 
39
- path = Pathname.new(File.expand_path(path))
40
- root_path = Pathname.new(root_path)
41
+ if File.absolute_path?(path)
42
+ # prefix_index is where the root path ends in the given path
43
+ prefix_index = root_path.size
44
+
45
+ # impossible case - absolute paths are returned from code coverage tool that always checks
46
+ # that root is a prefix of the path
47
+ return "" if path.size < prefix_index
48
+
49
+ prefix_index += 1 if path[prefix_index] == File::SEPARATOR
50
+ res = path[prefix_index..]
51
+ else
52
+ # prefix_to_root is a difference between the root path and the given path
53
+ if @prefix_to_root == ""
54
+ return path
55
+ elsif @prefix_to_root
56
+ return File.join(@prefix_to_root, path)
57
+ end
58
+
59
+ pathname = Pathname.new(File.expand_path(path))
60
+ root_path = Pathname.new(root_path)
61
+
62
+ # relative_path_from is an expensive function
63
+ res = pathname.relative_path_from(root_path).to_s
64
+
65
+ unless defined?(@prefix_to_root)
66
+ @prefix_to_root = res&.gsub(path, "") if res.end_with?(path)
67
+ end
68
+ end
41
69
 
42
- path.relative_path_from(root_path).to_s
70
+ res || ""
43
71
  end
44
72
 
45
73
  def self.repository_name
@@ -76,7 +76,7 @@ module Datadog
76
76
  # - tests that read files from disk
77
77
  # - tests that make network requests
78
78
  # - tests that call external processes
79
- # - tests that use forking or threading
79
+ # - tests that use forking
80
80
  #
81
81
  # @return [void]
82
82
  def itr_unskippable!
@@ -25,7 +25,9 @@ module Datadog
25
25
  class Component
26
26
  include Core::Utils::Forking
27
27
 
28
- attr_reader :correlation_id, :skippable_tests, :skipped_tests_count
28
+ attr_reader :correlation_id, :skippable_tests, :skippable_tests_fetch_error,
29
+ :skipped_tests_count, :total_tests_count,
30
+ :enabled, :test_skipping_enabled, :code_coverage_enabled
29
31
 
30
32
  def initialize(
31
33
  dd_env:,
@@ -58,7 +60,9 @@ module Datadog
58
60
  @correlation_id = nil
59
61
  @skippable_tests = Set.new
60
62
 
63
+ @total_tests_count = 0
61
64
  @skipped_tests_count = 0
65
+
62
66
  @mutex = Mutex.new
63
67
 
64
68
  Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
@@ -159,14 +163,16 @@ module Datadog
159
163
  end
160
164
 
161
165
  def count_skipped_test(test)
162
- return if !test.skipped? || !test.skipped_by_itr?
166
+ @mutex.synchronize do
167
+ @total_tests_count += 1
163
168
 
164
- if forked?
165
- Datadog.logger.warn { "Intelligent test runner is not supported for forking test runners yet" }
166
- return
167
- end
169
+ return if !test.skipped? || !test.skipped_by_itr?
170
+
171
+ if forked?
172
+ Datadog.logger.warn { "ITR is not supported for forking test runners yet" }
173
+ return
174
+ end
168
175
 
169
- @mutex.synchronize do
170
176
  Telemetry.itr_skipped
171
177
 
172
178
  @skipped_tests_count += 1
@@ -183,6 +189,10 @@ module Datadog
183
189
  test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_COUNT, @skipped_tests_count)
184
190
  end
185
191
 
192
+ def skippable_tests_count
193
+ skippable_tests.count
194
+ end
195
+
186
196
  def shutdown!
187
197
  @coverage_writer&.stop
188
198
  end
@@ -229,6 +239,7 @@ module Datadog
229
239
  skippable_response =
230
240
  Skippable.new(api: @api, dd_env: @dd_env, config_tags: @config_tags)
231
241
  .fetch_skippable_tests(test_session)
242
+ @skippable_tests_fetch_error = skippable_response.error_message unless skippable_response.ok?
232
243
 
233
244
  @correlation_id = skippable_response.correlation_id
234
245
  @skippable_tests = skippable_response.tests
@@ -42,6 +42,12 @@ module Datadog
42
42
  res
43
43
  end
44
44
 
45
+ def error_message
46
+ return nil if ok?
47
+
48
+ "Status code: #{@http_response&.code}, response: #{@http_response&.payload}"
49
+ end
50
+
45
51
  private
46
52
 
47
53
  def payload
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module TestOptimisation
6
+ module SkippablePercentage
7
+ class Base
8
+ attr_reader :failed
9
+
10
+ def initialize(verbose: false, spec_path: "spec")
11
+ @verbose = verbose
12
+ @spec_path = spec_path
13
+ @failed = false
14
+
15
+ log("Spec path: #{@spec_path}")
16
+ error!("Spec path is not a directory: #{@spec_path}") if !File.directory?(@spec_path)
17
+ end
18
+
19
+ def call
20
+ 0.0
21
+ end
22
+
23
+ private
24
+
25
+ def validate_test_optimisation_state!
26
+ unless test_optimisation.enabled
27
+ error!("ITR wasn't enabled, check the environment variables (DD_SERVICE, DD_ENV)")
28
+ end
29
+
30
+ if test_optimisation.skippable_tests_fetch_error
31
+ error!("Skippable tests couldn't be fetched, error: #{test_optimisation.skippable_tests_fetch_error}")
32
+ end
33
+ end
34
+
35
+ def log(message)
36
+ Datadog.logger.info(message) if @verbose
37
+ end
38
+
39
+ def error!(message)
40
+ Datadog.logger.error(message)
41
+ @failed = true
42
+ end
43
+
44
+ def test_optimisation
45
+ Datadog.send(:components).test_optimisation
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Datadog
6
+ module CI
7
+ module TestOptimisation
8
+ module SkippablePercentage
9
+ # This class calculates the percentage of tests that are going to be skipped in the next run
10
+ # without actually running the tests.
11
+ #
12
+ # It is useful to determine the number of parallel jobs that are required for the CI pipeline.
13
+ #
14
+ # NOTE: Only RSpec is supported at the moment.
15
+ class Calculator < Base
16
+ def initialize(rspec_cli_options: [], verbose: false, spec_path: "spec")
17
+ super(verbose: verbose, spec_path: spec_path)
18
+
19
+ @rspec_cli_options = rspec_cli_options || []
20
+ end
21
+
22
+ def call
23
+ return 0.0 if @failed
24
+
25
+ require_rspec!
26
+ return 0.0 if @failed
27
+
28
+ configure_datadog
29
+
30
+ exit_code = dry_run
31
+ if exit_code != 0
32
+ Datadog.logger.error("RSpec dry-run failed with exit code #{exit_code}")
33
+ @failed = true
34
+ return 0.0
35
+ end
36
+
37
+ log("Total tests count: #{test_optimisation.total_tests_count}")
38
+ log("Skipped tests count: #{test_optimisation.skipped_tests_count}")
39
+ validate_test_optimisation_state!
40
+
41
+ (test_optimisation.skipped_tests_count.to_f / test_optimisation.total_tests_count.to_f).floor(2)
42
+ end
43
+
44
+ private
45
+
46
+ def require_rspec!
47
+ require "rspec/core"
48
+ rescue LoadError
49
+ Datadog.logger.error("RSpec is not installed, currently this functionality is only supported for RSpec.")
50
+ @failed = true
51
+ end
52
+
53
+ def configure_datadog
54
+ Datadog.configure do |c|
55
+ c.ci.enabled = true
56
+ c.ci.itr_enabled = true
57
+ c.ci.retry_failed_tests_enabled = false
58
+ c.ci.retry_new_tests_enabled = false
59
+ c.ci.discard_traces = true
60
+ c.ci.instrument :rspec, dry_run_enabled: true
61
+ c.tracing.enabled = true
62
+ end
63
+ end
64
+
65
+ def dry_run
66
+ cli_options_array = @rspec_cli_options + ["--dry-run", @spec_path]
67
+
68
+ rspec_config_options = ::RSpec::Core::ConfigurationOptions.new(cli_options_array)
69
+ devnull = File.new("/dev/null", "w")
70
+ out = @verbose ? $stdout : devnull
71
+ err = @verbose ? $stderr : devnull
72
+
73
+ ::RSpec::Core::Runner.new(rspec_config_options).run(out, err)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Datadog
6
+ module CI
7
+ module TestOptimisation
8
+ module SkippablePercentage
9
+ # This class estimates the percentage of tests that are going to be skipped in the next run
10
+ # without actually running the tests. This estimate is very rough:
11
+ #
12
+ # - it counts the number of lines that start with "it" or "scenario" in the spec files, which could be inaccurate
13
+ # if you use shared examples
14
+ # - it only counts the number of tests that could be skipped, this does not mean that they will be actually skipped:
15
+ # if in this commit you replaced all the tests in your test suite with new ones, all the tests would be run (but
16
+ # this is highly unlikely)
17
+ #
18
+ # It is useful to determine the number of parallel jobs that are required for the CI pipeline.
19
+ #
20
+ # NOTE: Only RSpec is supported at the moment.
21
+ class Estimator < Base
22
+ def initialize(verbose: false, spec_path: "spec")
23
+ super
24
+ end
25
+
26
+ def call
27
+ return 0.0 if @failed
28
+
29
+ Datadog.configure do |c|
30
+ c.ci.enabled = true
31
+ c.ci.itr_enabled = true
32
+ c.ci.retry_failed_tests_enabled = false
33
+ c.ci.retry_new_tests_enabled = false
34
+ c.ci.discard_traces = true
35
+ c.tracing.enabled = true
36
+ end
37
+
38
+ spec_files = Dir["#{@spec_path}/**/*_spec.rb"]
39
+ estimated_tests_count = spec_files.sum do |file|
40
+ content = File.read(file)
41
+ content.scan(/(^\s*it\s+)|(^\s*scenario\s+)/).size
42
+ end
43
+
44
+ # starting and finishing a test session is required to get the skippable tests response
45
+ Datadog::CI.start_test_session(total_tests_count: estimated_tests_count)&.finish
46
+ skippable_tests_count = test_optimisation.skippable_tests_count
47
+
48
+ log("Estimated tests count: #{estimated_tests_count}")
49
+ log("Skippable tests count: #{skippable_tests_count}")
50
+ validate_test_optimisation_state!
51
+
52
+ [(skippable_tests_count.to_f / estimated_tests_count).floor(2), 0.99].min || 0.0
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module TestVisibility
6
+ class NullTransport
7
+ def initialize
8
+ end
9
+
10
+ def send_traces(traces)
11
+ []
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -23,6 +23,7 @@ module Datadog
23
23
  DEFAULT_TIMEOUT = 30
24
24
  MAX_RETRIES = 3
25
25
  INITIAL_BACKOFF = 1
26
+ MAX_BACKOFF = 30
26
27
 
27
28
  def initialize(host:, port:, timeout: DEFAULT_TIMEOUT, ssl: true, compress: false)
28
29
  @host = host
@@ -78,21 +79,48 @@ module Datadog
78
79
  private
79
80
 
80
81
  def perform_http_call(path:, payload:, headers:, verb:, retries: MAX_RETRIES, backoff: INITIAL_BACKOFF)
81
- adapter.call(
82
- path: path, payload: payload, headers: headers, verb: verb
83
- )
84
- rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, SocketError, Net::HTTPBadResponse => e
85
- Datadog.logger.debug("Failed to send request with #{e} (#{e.message})")
82
+ response = nil
83
+
84
+ begin
85
+ response = adapter.call(
86
+ path: path, payload: payload, headers: headers, verb: verb
87
+ )
88
+ return response if response.ok?
89
+
90
+ if response.code == 429
91
+ backoff = (response.header(Ext::Transport::HEADER_RATELIMIT_RESET) || 1).to_i
92
+
93
+ Datadog.logger.debug do
94
+ "Received rate limit response, retrying in #{backoff} seconds from X-RateLimit-Reset header"
95
+ end
96
+ elsif response.server_error?
97
+ Datadog.logger.debug { "Received server error response, retrying in #{backoff} seconds" }
98
+ else
99
+ return response
100
+ end
101
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, SocketError, Net::HTTPBadResponse => e
102
+ Datadog.logger.debug { "Failed to send request with #{e} (#{e.message})" }
86
103
 
87
- if retries.positive?
104
+ response = ErrorResponse.new(e)
105
+ end
106
+
107
+ if retries.positive? && backoff <= MAX_BACKOFF
88
108
  sleep(backoff)
89
109
 
90
110
  perform_http_call(
91
- path: path, payload: payload, headers: headers, verb: verb, retries: retries - 1, backoff: backoff * 2
111
+ path: path,
112
+ payload: payload,
113
+ headers: headers,
114
+ verb: verb,
115
+ retries: retries - 1,
116
+ backoff: backoff * 2
92
117
  )
93
118
  else
94
- Datadog.logger.error("Failed to send request after #{MAX_RETRIES} retries")
95
- ErrorResponse.new(e)
119
+ Datadog.logger.error(
120
+ "Failed to send request after #{MAX_RETRIES - retries} retries (current backoff value #{backoff})"
121
+ )
122
+
123
+ response
96
124
  end
97
125
  end
98
126
 
@@ -121,6 +149,10 @@ module Datadog
121
149
  nil
122
150
  end
123
151
 
152
+ def response_size
153
+ 0
154
+ end
155
+
124
156
  def inspect
125
157
  "ErrorResponse error:#{error}"
126
158
  end
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 7
7
+ MINOR = 8
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-25 00:00:00.000000000 Z
11
+ date: 2024-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: datadog
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.3'
19
+ version: '2.4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.3'
26
+ version: '2.4'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: msgpack
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,7 +44,8 @@ description: |2
44
44
  their CI pipelines.
45
45
  email:
46
46
  - dev@datadoghq.com
47
- executables: []
47
+ executables:
48
+ - ddcirb
48
49
  extensions:
49
50
  - ext/datadog_cov/extconf.rb
50
51
  extra_rdoc_files: []
@@ -56,9 +57,14 @@ files:
56
57
  - LICENSE.BSD3
57
58
  - NOTICE
58
59
  - README.md
60
+ - exe/ddcirb
59
61
  - ext/datadog_cov/datadog_cov.c
60
62
  - ext/datadog_cov/extconf.rb
61
63
  - lib/datadog/ci.rb
64
+ - lib/datadog/ci/cli/cli.rb
65
+ - lib/datadog/ci/cli/command/base.rb
66
+ - lib/datadog/ci/cli/command/skippable_tests_percentage.rb
67
+ - lib/datadog/ci/cli/command/skippable_tests_percentage_estimate.rb
62
68
  - lib/datadog/ci/codeowners/matcher.rb
63
69
  - lib/datadog/ci/codeowners/parser.rb
64
70
  - lib/datadog/ci/codeowners/rule.rb
@@ -154,6 +160,9 @@ files:
154
160
  - lib/datadog/ci/test_optimisation/coverage/transport.rb
155
161
  - lib/datadog/ci/test_optimisation/coverage/writer.rb
156
162
  - lib/datadog/ci/test_optimisation/skippable.rb
163
+ - lib/datadog/ci/test_optimisation/skippable_percentage/base.rb
164
+ - lib/datadog/ci/test_optimisation/skippable_percentage/calculator.rb
165
+ - lib/datadog/ci/test_optimisation/skippable_percentage/estimator.rb
157
166
  - lib/datadog/ci/test_optimisation/telemetry.rb
158
167
  - lib/datadog/ci/test_retries/component.rb
159
168
  - lib/datadog/ci/test_retries/driver/base.rb
@@ -172,6 +181,7 @@ files:
172
181
  - lib/datadog/ci/test_visibility/context.rb
173
182
  - lib/datadog/ci/test_visibility/flush.rb
174
183
  - lib/datadog/ci/test_visibility/null_component.rb
184
+ - lib/datadog/ci/test_visibility/null_transport.rb
175
185
  - lib/datadog/ci/test_visibility/serializers/base.rb
176
186
  - lib/datadog/ci/test_visibility/serializers/factories/test_level.rb
177
187
  - lib/datadog/ci/test_visibility/serializers/factories/test_suite_level.rb