datadog-ci 1.33.0 → 1.34.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e5dd4e5fdf647baf9134e9fdcc5eb6d1215c48b08bf1905dd69e731a8ebe18e
4
- data.tar.gz: 851eadff7367e69480490eea9fb7e5e074c2099008de2537c07efba2de9366bf
3
+ metadata.gz: d2adf2fb252fdff7c6d0e1e73c2b6ade74c53bc5e8a238d447793da6833837c0
4
+ data.tar.gz: a49b2d09ae48bb2dc90c9846c6aae128a05d75353a2e7dc9df248960f165f556
5
5
  SHA512:
6
- metadata.gz: 22bedcab49da1df468ae63d217b160d7e3a976a5b722aaa012622564629d8760afc39165b47dc8be7454b406db8e073be46524d0de02fa2b6fd13a4e685713c8
7
- data.tar.gz: 9affe9fe044871139755c3fbbb0b28146bc07c2b36ed79627ad0db67ee254764fbca12bebafc9a317224bd275f9c60e5a978a3c526b8a11c0a4a92150152f104
6
+ metadata.gz: 75d99396decbd20bb93cc2e061295be1e55c033b41980216bc0bb9229fd36269e7ad8bd5ad1104203ca10a32bed668aa65f41d01ddd5c8ebef34d206397a7c25
7
+ data.tar.gz: 3a0efeaaad0042cd836c51a7232a0348b377acf959bc6e10670e01ed5d7ea863caf60853142cb7e2d563da1c64ef1dadee5c3735a69bf482dfdb7b611239767d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.34.0] - 2026-06-25
4
+
5
+ ### Added
6
+
7
+ * Add configurable TIA skipping mode ([#530][])
8
+
3
9
  ## [1.33.0] - 2026-06-19
4
10
 
5
11
  ### Added
@@ -659,7 +665,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
659
665
 
660
666
  - Ruby versions < 2.7 no longer supported ([#8][])
661
667
 
662
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.33.0...main
668
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...main
669
+ [1.34.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.33.0...v1.34.0
663
670
  [1.33.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.32.0...v1.33.0
664
671
  [1.32.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.31.0...v1.32.0
665
672
  [1.31.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.30.0...v1.31.0
@@ -933,4 +940,5 @@ Currently test suite level visibility is not used by our instrumentation: it wil
933
940
  [#516]: https://github.com/DataDog/datadog-ci-rb/issues/516
934
941
  [#521]: https://github.com/DataDog/datadog-ci-rb/issues/521
935
942
  [#522]: https://github.com/DataDog/datadog-ci-rb/issues/522
936
- [#527]: https://github.com/DataDog/datadog-ci-rb/issues/527
943
+ [#527]: https://github.com/DataDog/datadog-ci-rb/issues/527
944
+ [#530]: https://github.com/DataDog/datadog-ci-rb/issues/530
@@ -210,6 +210,7 @@ module Datadog
210
210
  api: test_visibility_api,
211
211
  dd_env: settings.env,
212
212
  config_tags: custom_configuration(settings),
213
+ test_skipping_mode: settings.ci.tia_test_skipping_mode,
213
214
  coverage_writer: build_coverage_writer(settings, test_visibility_api),
214
215
  enabled: settings.ci.enabled && settings.ci.itr_enabled,
215
216
  bundle_location: settings.ci.itr_code_coverage_excluded_bundle_path,
@@ -293,6 +294,7 @@ module Datadog
293
294
  Remote::LibrarySettingsClient.new(
294
295
  api: api,
295
296
  dd_env: settings.env,
297
+ test_skipping_mode: settings.ci.tia_test_skipping_mode,
296
298
  config_tags: custom_configuration(settings)
297
299
  )
298
300
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  require_relative "../contrib/instrumentation"
4
4
  require_relative "../ext/settings"
5
+ require_relative "../ext/test"
5
6
  require_relative "../ext/test_optimization_cache"
6
7
  require_relative "../utils/bundle"
8
+ require_relative "../utils/configuration"
7
9
  require_relative "../utils/parsing"
8
10
 
9
11
  module Datadog
@@ -79,6 +81,15 @@ module Datadog
79
81
  o.default true
80
82
  end
81
83
 
84
+ option :tia_test_skipping_mode do |o|
85
+ o.type :string
86
+ o.env CI::Ext::Settings::ENV_TIA_TEST_SKIPPING_MODE
87
+ o.default CI::Ext::Test::TIATestSkippingMode::TEST
88
+ o.setter do |mode|
89
+ Utils::Configuration.normalize_tia_test_skipping_mode(mode)
90
+ end
91
+ end
92
+
82
93
  option :git_metadata_upload_enabled do |o|
83
94
  o.type :bool
84
95
  o.env CI::Ext::Settings::ENV_GIT_METADATA_UPLOAD_ENABLED
@@ -72,9 +72,14 @@ module Datadog
72
72
  end
73
73
 
74
74
  unless same_test_suite_as_current?(test_suite_name)
75
+ suite_tags = test_suite_source_file_tags(event.test_case)
76
+ if test_suite_unskippable?(event.test_case)
77
+ suite_tags[CI::Ext::Test::TAG_ITR_UNSKIPPABLE] = "true"
78
+ end
79
+
75
80
  start_test_suite(
76
81
  test_suite_name,
77
- tags: test_suite_source_file_tags(event.test_case)
82
+ tags: suite_tags
78
83
  )
79
84
  end
80
85
 
@@ -228,6 +233,51 @@ module Datadog
228
233
  CI::Ext::Test::TAG_SOURCE_START => line_number.to_s
229
234
  }
230
235
  end
236
+
237
+ def test_suite_unskippable?(test_case)
238
+ feature = if test_case.respond_to?(:feature)
239
+ test_case.feature
240
+ else
241
+ @ast_lookup&.gherkin_document(test_case.location.file)&.feature
242
+ end
243
+
244
+ node_has_unskippable_tag?(feature) || test_case_unskippable?(test_case)
245
+ rescue
246
+ test_case_unskippable?(test_case)
247
+ end
248
+
249
+ def node_has_unskippable_tag?(node)
250
+ return false if node.nil?
251
+ return true if node_tags_include_unskippable?(node)
252
+
253
+ if node.respond_to?(:scenario) && node.scenario
254
+ return true if node_has_unskippable_tag?(node.scenario)
255
+ end
256
+
257
+ if node.respond_to?(:rule) && node.rule
258
+ return true if node_has_unskippable_tag?(node.rule)
259
+ end
260
+
261
+ return false unless node.respond_to?(:children)
262
+
263
+ node.children.any? { |child| node_has_unskippable_tag?(child) }
264
+ end
265
+
266
+ def node_tags_include_unskippable?(node)
267
+ return false unless node.respond_to?(:tags)
268
+
269
+ node.tags.any? { |tag| tag.respond_to?(:name) && tag.name == datadog_itr_unskippable_tag }
270
+ end
271
+
272
+ def test_case_unskippable?(test_case)
273
+ test_case.match_tags?(datadog_itr_unskippable_tag)
274
+ rescue
275
+ false
276
+ end
277
+
278
+ def datadog_itr_unskippable_tag
279
+ "@#{CI::Ext::Test::ITR_UNSKIPPABLE_OPTION}"
280
+ end
231
281
  end
232
282
  end
233
283
  end
@@ -37,6 +37,11 @@ module Datadog
37
37
  end
38
38
 
39
39
  def begin_scenario(test_case)
40
+ active_suite = Datadog::CI.active_test_suite
41
+ if active_suite&.should_skip?
42
+ raise ::Cucumber::Core::Test::Result::Skipped, active_suite.datadog_skip_reason
43
+ end
44
+
40
45
  datadog_test = Datadog::CI.active_test
41
46
 
42
47
  # special case for cucumber-ruby: we skip quarantined tests, thus for cucumber quarantined is the same as disabled.
@@ -7,6 +7,31 @@ module Datadog
7
7
  module Contrib
8
8
  module Minitest
9
9
  module Helpers
10
+ module RunnableClassMethods
11
+ def datadog_itr_unskippable(*args)
12
+ if args.nil? || args.empty?
13
+ @datadog_itr_unskippable_suite = true
14
+ else
15
+ @datadog_itr_unskippable_tests = args
16
+ end
17
+ end
18
+
19
+ def dd_suite_unskippable?
20
+ @datadog_itr_unskippable_suite
21
+ end
22
+
23
+ def dd_test_unskippable?(test_name)
24
+ tests = @datadog_itr_unskippable_tests
25
+ return false unless tests
26
+
27
+ tests.include?(test_name)
28
+ end
29
+
30
+ def dd_any_unskippable?
31
+ dd_suite_unskippable? || !!@datadog_itr_unskippable_tests
32
+ end
33
+ end
34
+
10
35
  def self.start_test_suite(klass)
11
36
  method = klass.runnable_methods.first
12
37
  return nil if method.nil?
@@ -22,6 +47,9 @@ module Datadog
22
47
  else
23
48
  {}
24
49
  end
50
+ if klass.dd_any_unskippable?
51
+ test_suite_tags[CI::Ext::Test::TAG_ITR_UNSKIPPABLE] = "true"
52
+ end
25
53
 
26
54
  test_tracing_component = Datadog.send(:components).test_tracing
27
55
  test_suite = test_tracing_component.start_test_suite(
@@ -60,6 +88,11 @@ module Datadog
60
88
  source_location
61
89
  end
62
90
 
91
+ def self.skip_test_suite(test_suite)
92
+ test_suite&.finish
93
+ []
94
+ end
95
+
63
96
  def self.parallel?(klass)
64
97
  klass.ancestors.include?(::Minitest::Parallel::Test) || ci_queue?
65
98
  end
@@ -10,11 +10,16 @@ module Datadog
10
10
  end
11
11
 
12
12
  module ClassMethods
13
+ include Helpers::RunnableClassMethods
14
+
13
15
  def run(*args)
14
16
  return super unless datadog_configuration[:enabled]
15
17
  return super if Helpers.parallel?(self)
16
18
 
17
19
  test_suite = Helpers.start_test_suite(self)
20
+ if test_suite&.should_skip?
21
+ return Helpers.skip_test_suite(test_suite)
22
+ end
18
23
 
19
24
  results = super
20
25
  return results unless test_suite
@@ -10,11 +10,16 @@ module Datadog
10
10
  end
11
11
 
12
12
  module ClassMethods
13
+ include Helpers::RunnableClassMethods
14
+
13
15
  def run_suite(*args)
14
16
  return super unless datadog_configuration[:enabled]
15
17
  return super if Helpers.parallel?(self)
16
18
 
17
19
  test_suite = Helpers.start_test_suite(self)
20
+ if test_suite&.should_skip?
21
+ return Helpers.skip_test_suite(test_suite)
22
+ end
18
23
 
19
24
  results = super
20
25
  return results unless test_suite
@@ -37,6 +37,11 @@ module Datadog
37
37
 
38
38
  return run_without_datadog_reentry if datadog_run_reentered?
39
39
 
40
+ test_suite = start_datadog_test_suite_if_parallel
41
+ if test_suite&.should_skip?
42
+ return skip_datadog_suite(test_suite)
43
+ end
44
+
40
45
  test_span = start_datadog_test
41
46
  return skip_datadog_test(test_span) if test_span&.should_skip?
42
47
 
@@ -76,10 +81,6 @@ module Datadog
76
81
  end
77
82
 
78
83
  def start_datadog_test
79
- if Helpers.parallel?(self.class)
80
- Helpers.start_test_suite(self.class)
81
- end
82
-
83
84
  test_suite_name = Helpers.test_suite_name(self.class, name)
84
85
 
85
86
  # @type var tags : Hash[String, String]
@@ -103,15 +104,17 @@ module Datadog
103
104
  tags: tags,
104
105
  service: datadog_configuration[:service_name]
105
106
  )
106
- # Steep type checker doesn't know that we patched Minitest::Test class definition
107
- #
108
- # steep:ignore:start
109
107
  test_span&.itr_unskippable! if self.class.dd_suite_unskippable? || self.class.dd_test_unskippable?(name)
110
- # steep:ignore:end
111
108
 
112
109
  test_span
113
110
  end
114
111
 
112
+ def start_datadog_test_suite_if_parallel
113
+ return unless Helpers.parallel?(self.class)
114
+
115
+ Helpers.start_test_suite(self.class)
116
+ end
117
+
115
118
  def skip_datadog_test(test_span)
116
119
  time_it do
117
120
  capture_exceptions do
@@ -124,6 +127,16 @@ module Datadog
124
127
  ::Minitest::Result.from(self)
125
128
  end
126
129
 
130
+ def skip_datadog_suite(test_suite)
131
+ time_it do
132
+ capture_exceptions do
133
+ skip(test_suite.datadog_skip_reason)
134
+ end
135
+ end
136
+
137
+ ::Minitest::Result.from(self)
138
+ end
139
+
127
140
  def finish_with_result(span, result_code)
128
141
  return unless span
129
142
 
@@ -158,25 +171,6 @@ module Datadog
158
171
  ensure
159
172
  super
160
173
  end
161
-
162
- def datadog_itr_unskippable(*args)
163
- if args.nil? || args.empty?
164
- @datadog_itr_unskippable_suite = true
165
- else
166
- @datadog_itr_unskippable_tests = args
167
- end
168
- end
169
-
170
- def dd_suite_unskippable?
171
- @datadog_itr_unskippable_suite
172
- end
173
-
174
- def dd_test_unskippable?(test_name)
175
- tests = @datadog_itr_unskippable_tests
176
- return false unless tests
177
-
178
- tests.include?(test_name)
179
- end
180
174
  end
181
175
  end
182
176
  end
@@ -29,13 +29,9 @@ module Datadog
29
29
  return unless defined?(super)
30
30
 
31
31
  patch_only_once.run do
32
- # There is no way to tell Steep that we prepend these methods to modules that have .patch method
33
- #
34
- # steep:ignore:start
35
32
  super.tap do
36
33
  @patch_successful = true
37
34
  end
38
- # steep:ignore:end
39
35
  rescue => e
40
36
  on_patch_error(e)
41
37
  end
@@ -24,7 +24,7 @@ module Datadog
24
24
  # even if it is a nested context
25
25
  metadata[:skip] = true if all_examples_skipped_by_datadog?
26
26
 
27
- # Start context coverage for this example group (for TIA suite-level coverage).
27
+ # Start context coverage for this example group (for TIA context coverage).
28
28
  # This captures code executed in before(:context)/before(:all) hooks.
29
29
  # The context_id uses scoped_id which is a stable identifier for RSpec example groups.
30
30
  context_id = datadog_context_id
@@ -39,6 +39,9 @@ module Datadog
39
39
  CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(metadata[:file_path]),
40
40
  CI::Ext::Test::TAG_SOURCE_START => metadata[:line_number].to_s
41
41
  }
42
+ if descendant_filtered_examples.any?(&:datadog_unskippable?)
43
+ suite_tags = suite_tags.merge(CI::Ext::Test::TAG_ITR_UNSKIPPABLE => "true")
44
+ end
42
45
 
43
46
  test_suite =
44
47
  test_tracing_component&.start_test_suite(
@@ -47,6 +50,8 @@ module Datadog
47
50
  service: datadog_configuration[:service_name]
48
51
  )
49
52
 
53
+ return skip_test_suite(test_suite) if test_suite&.should_skip?
54
+
50
55
  success = super
51
56
 
52
57
  return success unless test_suite
@@ -61,6 +66,11 @@ module Datadog
61
66
 
62
67
  private
63
68
 
69
+ def skip_test_suite(test_suite)
70
+ test_suite&.finish
71
+ true
72
+ end
73
+
64
74
  def all_examples_skipped_by_datadog?
65
75
  descendant_filtered_examples.all? do |example|
66
76
  datadog_test_id = example.datadog_test_id
@@ -12,6 +12,7 @@ module Datadog
12
12
  ENV_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED = "DD_CIVISIBILITY_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED"
13
13
  ENV_FORCE_TEST_LEVEL_VISIBILITY = "DD_CIVISIBILITY_FORCE_TEST_LEVEL_VISIBILITY"
14
14
  ENV_ITR_ENABLED = "DD_CIVISIBILITY_ITR_ENABLED"
15
+ ENV_TIA_TEST_SKIPPING_MODE = "DD_TESTOPTIMIZATION_TIA_TEST_SKIPPING_MODE"
15
16
  ENV_GIT_METADATA_UPLOAD_ENABLED = "DD_CIVISIBILITY_GIT_METADATA_UPLOAD_ENABLED"
16
17
  ENV_ITR_CODE_COVERAGE_EXCLUDED_BUNDLE_PATH = "DD_CIVISIBILITY_ITR_CODE_COVERAGE_EXCLUDED_BUNDLE_PATH"
17
18
  ENV_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE = "DD_CIVISIBILITY_ITR_CODE_COVERAGE_USE_SINGLE_THREADED_MODE"
@@ -44,6 +44,7 @@ module Datadog
44
44
  METRIC_ITR_SKIPPABLE_TESTS_REQUEST_ERRORS = "itr_skippable_tests.request_errors"
45
45
  METRIC_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES = "itr_skippable_tests.response_bytes"
46
46
  METRIC_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS = "itr_skippable_tests.response_tests"
47
+ METRIC_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES = "itr_skippable_tests.response_suites"
47
48
 
48
49
  METRIC_ITR_SKIPPED = "itr_skipped"
49
50
  METRIC_ITR_UNSKIPPABLE = "itr_unskippable"
@@ -162,7 +162,14 @@ module Datadog
162
162
  ].freeze
163
163
 
164
164
  # could be either "test" or "suite" depending on whether we skip individual tests or whole suites
165
- ITR_TEST_SKIPPING_MODE = "test" # we always skip tests (not suites) in Ruby
165
+ module TIATestSkippingMode
166
+ TEST = "test"
167
+ SUITE = "suite"
168
+
169
+ ALL = [TEST, SUITE].freeze
170
+ end
171
+
172
+ DEFAULT_TIA_TEST_SKIPPING_MODE = TIATestSkippingMode::TEST
166
173
  ITR_UNSKIPPABLE_OPTION = :datadog_itr_unskippable
167
174
 
168
175
  EARLY_FLAKE_FAULTY = "faulty"
@@ -17,10 +17,11 @@ module Datadog
17
17
  module Remote
18
18
  # Calls settings endpoint to fetch library settings for given service and env
19
19
  class LibrarySettingsClient
20
- def initialize(dd_env:, api: nil, config_tags: {})
20
+ def initialize(dd_env:, api: nil, config_tags: {}, test_skipping_mode: Ext::Test::TIATestSkippingMode::TEST)
21
21
  @api = api
22
22
  @dd_env = dd_env
23
23
  @config_tags = config_tags || {}
24
+ @test_skipping_mode = test_skipping_mode
24
25
  end
25
26
 
26
27
  def fetch(test_session)
@@ -87,7 +88,7 @@ module Datadog
87
88
  "repository_url" => test_session.git_repository_url,
88
89
  "branch" => test_session.git_branch || test_session.git_tag,
89
90
  "sha" => test_session.git_commit_sha,
90
- "test_level" => Ext::Test::ITR_TEST_SKIPPING_MODE,
91
+ "test_level" => @test_skipping_mode,
91
92
  "configurations" => {
92
93
  Ext::Test::TAG_OS_PLATFORM => test_session.os_platform,
93
94
  Ext::Test::TAG_OS_ARCHITECTURE => test_session.os_architecture,
@@ -16,11 +16,8 @@ module Datadog
16
16
  return nil if target.nil?
17
17
  return nil unless LAST_LINE_AVAILABLE
18
18
 
19
- # Ruby has outdated RBS for RubyVM::InstructionSequence where method `of` is not defined
20
- # steep:ignore:start
21
19
  iseq = RubyVM::InstructionSequence.of(target)
22
20
  return nil unless iseq.is_a?(RubyVM::InstructionSequence)
23
- # steep:ignore:end
24
21
 
25
22
  # this function is implemented in ext/datadog_ci_native/datadog_method_inspect.c
26
23
  _native_last_line_from_iseq(iseq)
@@ -30,8 +30,8 @@ module Datadog
30
30
 
31
31
  FILE_STORAGE_KEY = "test_impact_analysis_component_state"
32
32
 
33
- attr_reader :correlation_id, :skippable_tests, :skippable_tests_fetch_error,
34
- :enabled, :test_skipping_enabled, :code_coverage_enabled
33
+ attr_reader :correlation_id, :skippable_tests, :skippable_suites, :skippable_tests_fetch_error,
34
+ :enabled, :test_skipping_enabled, :code_coverage_enabled, :test_skipping_mode
35
35
 
36
36
  def initialize(
37
37
  dd_env:,
@@ -39,6 +39,7 @@ module Datadog
39
39
  api: nil,
40
40
  coverage_writer: nil,
41
41
  enabled: false,
42
+ test_skipping_mode: Ext::Test::TIATestSkippingMode::TEST,
42
43
  bundle_location: nil,
43
44
  use_single_threaded_coverage: false,
44
45
  use_allocation_tracing: true,
@@ -48,6 +49,7 @@ module Datadog
48
49
  @api = api
49
50
  @dd_env = dd_env
50
51
  @config_tags = config_tags || {}
52
+ @test_skipping_mode = test_skipping_mode
51
53
 
52
54
  @bundle_location = if bundle_location && !File.absolute_path?(bundle_location)
53
55
  File.join(Git::LocalRepository.root, bundle_location)
@@ -65,6 +67,7 @@ module Datadog
65
67
 
66
68
  @correlation_id = nil
67
69
  @skippable_tests = Set.new
70
+ @skippable_suites = Set.new
68
71
 
69
72
  @mutex = Mutex.new
70
73
 
@@ -92,8 +95,7 @@ module Datadog
92
95
 
93
96
  test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED, @test_skipping_enabled)
94
97
  test_session.set_tag(Ext::Test::TAG_CODE_COVERAGE_ENABLED, @code_coverage_enabled)
95
- # we skip tests, not suites
96
- test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, Ext::Test::ITR_TEST_SKIPPING_MODE)
98
+ test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, @test_skipping_mode)
97
99
 
98
100
  if @code_coverage_enabled
99
101
  load_datadog_cov!
@@ -102,10 +104,10 @@ module Datadog
102
104
  end
103
105
 
104
106
  # Load external cache or component state first, and if successful, skip fetching skippable tests
105
- if skipping_tests?
107
+ if skipping_tests? || skipping_suites?
106
108
  return if load_component_state
107
109
 
108
- fetch_skippable_tests(test_session)
110
+ fetch_skippables(test_session)
109
111
  store_component_state if test_session.distributed
110
112
  end
111
113
 
@@ -117,7 +119,19 @@ module Datadog
117
119
  end
118
120
 
119
121
  def skipping_tests?
120
- @test_skipping_enabled
122
+ @test_skipping_enabled && test_skipping_mode?
123
+ end
124
+
125
+ def skipping_suites?
126
+ @test_skipping_enabled && suite_skipping_mode?
127
+ end
128
+
129
+ def test_skipping_mode?
130
+ @test_skipping_mode == Ext::Test::TIATestSkippingMode::TEST
131
+ end
132
+
133
+ def suite_skipping_mode?
134
+ @test_skipping_mode == Ext::Test::TIATestSkippingMode::SUITE
121
135
  end
122
136
 
123
137
  def code_coverage?
@@ -174,6 +188,7 @@ module Datadog
174
188
  # @return [void]
175
189
  def on_test_started(test)
176
190
  return if !enabled? || !code_coverage?
191
+ return if suite_skipping_mode?
177
192
 
178
193
  # Stop any in-progress context coverage and store it
179
194
  stop_context_coverage_and_store
@@ -200,6 +215,7 @@ module Datadog
200
215
  # @return [Datadog::CI::TestImpactAnalysis::Coverage::Event, nil] The coverage event or nil
201
216
  def on_test_finished(test, context)
202
217
  return unless enabled?
218
+ return if suite_skipping_mode?
203
219
 
204
220
  # Handle ITR statistics
205
221
  if test.skipped_by_test_impact_analysis?
@@ -222,33 +238,13 @@ module Datadog
222
238
  context_ids = test.context_ids || []
223
239
  merge_context_coverages_into_test(coverage, context_ids)
224
240
 
225
- if coverage.empty?
226
- Telemetry.code_coverage_is_empty
227
- return
228
- end
229
-
230
- # cucumber's gherkin files are not covered by the code coverage collector - we add them here explicitly
231
- test_source_file = test.source_file
232
- ensure_test_source_covered(test_source_file, coverage) unless test_source_file.nil?
233
-
234
- # if we have static dependencies tracking enabled then we can make the coverage
235
- # more precise by fetching which files we depend on based on constants usage
236
- enrich_coverage_with_static_dependencies(coverage)
237
-
238
- Telemetry.code_coverage_files(coverage.size)
239
-
240
- coverage_event = Coverage::Event.new(
241
+ write_coverage_event(
241
242
  test_id: test.id.to_s,
242
243
  test_suite_id: test.test_suite_id.to_s,
243
244
  test_session_id: test.test_session_id.to_s,
245
+ source_file: test.source_file,
244
246
  coverage: coverage
245
247
  )
246
-
247
- Datadog.logger.debug { "Writing coverage event \n #{coverage_event.pretty_inspect}" }
248
-
249
- write(coverage_event)
250
-
251
- coverage_event
252
248
  end
253
249
 
254
250
  # Clears stored context coverage for a specific context.
@@ -271,7 +267,7 @@ module Datadog
271
267
  #
272
268
  # @return [Boolean]
273
269
  def context_coverage_enabled?
274
- enabled? && code_coverage? && !@use_single_threaded_coverage
270
+ enabled? && code_coverage? && !suite_skipping_mode? && !@use_single_threaded_coverage
275
271
  end
276
272
 
277
273
  def skippable?(datadog_test_id)
@@ -280,6 +276,12 @@ module Datadog
280
276
  @mutex.synchronize { @skippable_tests.include?(datadog_test_id) }
281
277
  end
282
278
 
279
+ def skippable_suite?(test_suite_name)
280
+ return false if !enabled? || !skipping_suites?
281
+
282
+ @mutex.synchronize { @skippable_suites.include?(test_suite_name) }
283
+ end
284
+
283
285
  def mark_if_skippable(test)
284
286
  return if !enabled? || !skipping_tests?
285
287
 
@@ -292,6 +294,66 @@ module Datadog
292
294
  end
293
295
  end
294
296
 
297
+ def mark_if_suite_skippable(test_suite)
298
+ return if !enabled? || !skipping_suites?
299
+
300
+ unskippable = test_suite.itr_unskippable?
301
+ Telemetry.itr_unskippable if unskippable
302
+
303
+ unless skippable_suite?(test_suite.name)
304
+ Datadog.logger.debug { "Test suite is not skippable: #{test_suite.name}" }
305
+ return
306
+ end
307
+
308
+ if unskippable
309
+ Telemetry.itr_forced_run
310
+ test_suite.set_tag(Ext::Test::TAG_ITR_FORCED_RUN, "true")
311
+
312
+ Datadog.logger.debug { "Forced run of skippable test suite: #{test_suite.name}" }
313
+ return
314
+ end
315
+
316
+ test_suite.set_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR, "true")
317
+ test_suite.skipped!(reason: Ext::Test::SkipReason::TEST_IMPACT_ANALYSIS)
318
+
319
+ Datadog.logger.debug { "Marked test suite as skippable: #{test_suite.name}" }
320
+ end
321
+
322
+ def on_test_suite_started(test_suite)
323
+ return unless enabled? && suite_skipping_mode?
324
+
325
+ mark_if_suite_skippable(test_suite)
326
+ return if test_suite.should_skip?
327
+ return unless code_coverage?
328
+
329
+ Telemetry.code_coverage_started(test_suite)
330
+ coverage_collector&.start
331
+ end
332
+
333
+ def on_test_suite_finished(test_suite, context)
334
+ return unless enabled? && suite_skipping_mode?
335
+
336
+ if test_suite.skipped_by_test_impact_analysis?
337
+ Telemetry.itr_skipped
338
+ context.incr_tests_skipped_by_tia_count
339
+ return
340
+ end
341
+
342
+ return unless code_coverage?
343
+
344
+ Telemetry.code_coverage_finished(test_suite)
345
+
346
+ coverage = coverage_collector&.stop
347
+
348
+ write_coverage_event(
349
+ test_id: nil,
350
+ test_suite_id: test_suite.id.to_s,
351
+ test_session_id: test_suite.get_tag(Ext::Test::TAG_TEST_SESSION_ID).to_s,
352
+ source_file: test_suite.source_file,
353
+ coverage: coverage
354
+ )
355
+ end
356
+
295
357
  def write_test_session_tags(test_session, skipped_tests_count)
296
358
  return if !enabled?
297
359
 
@@ -302,8 +364,8 @@ module Datadog
302
364
  test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_COUNT, skipped_tests_count)
303
365
  end
304
366
 
305
- def skippable_tests_count
306
- skippable_tests.count
367
+ def skippables_count
368
+ current_skippables.count
307
369
  end
308
370
 
309
371
  def shutdown!
@@ -314,15 +376,17 @@ module Datadog
314
376
  def serialize_state
315
377
  {
316
378
  correlation_id: @correlation_id,
317
- skippable_tests: @skippable_tests
379
+ skippable_tests: @skippable_tests,
380
+ skippable_suites: @skippable_suites
318
381
  }
319
382
  end
320
383
 
321
384
  def restore_state(state)
322
- @mutex.synchronize do
323
- @correlation_id = state[:correlation_id]
324
- @skippable_tests = state[:skippable_tests]
325
- end
385
+ set_skippables(
386
+ correlation_id: state[:correlation_id],
387
+ tests: state[:skippable_tests] || Set.new,
388
+ suites: state[:skippable_suites] || Set.new
389
+ )
326
390
  end
327
391
 
328
392
  def storage_key
@@ -330,24 +394,20 @@ module Datadog
330
394
  end
331
395
 
332
396
  def restore_state_from_datadog_test_runner
333
- Datadog.logger.debug { "Restoring skippable tests from Test Optimization cache" }
397
+ Datadog.logger.debug { "Restoring skippables from Test Optimization cache" }
334
398
 
335
- skippable_tests_data = load_cached_skippable_tests
336
- if skippable_tests_data.nil?
337
- Datadog.logger.debug { "Restoring skippable tests failed, will request again" }
399
+ skippables_data = load_cached_skippable_tests
400
+ if skippables_data.nil?
401
+ Datadog.logger.debug { "Restoring skippables failed, will request again" }
338
402
  return false
339
403
  end
340
404
 
341
- Datadog.logger.debug { "Restored skippable tests from Test Optimization: #{skippable_tests_data}" }
405
+ Datadog.logger.debug { "Restored skippables from Test Optimization: #{skippables_data}" }
342
406
 
343
- skippable_response = Skippable::Response.from_json(skippable_tests_data)
407
+ skippable_response = Skippable::Response.from_json(skippables_data)
408
+ apply_skippable_response(skippable_response)
344
409
 
345
- @mutex.synchronize do
346
- @correlation_id = skippable_response.correlation_id
347
- @skippable_tests = skippable_response.tests
348
- end
349
-
350
- Datadog.logger.debug { "Found [#{@skippable_tests.size}] skippable tests from context" }
410
+ Datadog.logger.debug { "Found [#{skippables_count}] skippable #{@test_skipping_mode}s from context" }
351
411
  Datadog.logger.debug { "ITR correlation ID from context: #{@correlation_id}" }
352
412
 
353
413
  true
@@ -406,27 +466,84 @@ module Datadog
406
466
  coverage[absolute_test_source_file_path] = true
407
467
  end
408
468
 
409
- def fetch_skippable_tests(test_session)
410
- return unless skipping_tests?
469
+ def write_coverage_event(test_id:, test_suite_id:, test_session_id:, source_file:, coverage:)
470
+ coverage ||= {}
471
+ if coverage.empty?
472
+ Telemetry.code_coverage_is_empty
473
+ return
474
+ end
475
+
476
+ ensure_test_source_covered(source_file, coverage) unless source_file.nil?
477
+
478
+ enrich_coverage_with_static_dependencies(coverage)
479
+
480
+ Telemetry.code_coverage_files(coverage.size)
481
+
482
+ coverage_event = Coverage::Event.new(
483
+ test_id: test_id,
484
+ test_suite_id: test_suite_id,
485
+ test_session_id: test_session_id,
486
+ coverage: coverage
487
+ )
488
+
489
+ Datadog.logger.debug { "Writing coverage event \n #{coverage_event.pretty_inspect}" }
490
+
491
+ write(coverage_event)
492
+
493
+ coverage_event
494
+ end
495
+
496
+ def fetch_skippables(test_session)
497
+ return unless skipping_tests? || skipping_suites?
411
498
 
412
499
  # we can only request skippable tests if git metadata is already uploaded
413
500
  git_tree_upload_worker.wait_until_done
414
501
 
415
502
  skippable_response =
416
- Skippable.new(api: @api, dd_env: @dd_env, config_tags: @config_tags)
417
- .fetch_skippable_tests(test_session)
503
+ Skippable.new(
504
+ api: @api,
505
+ dd_env: @dd_env,
506
+ config_tags: @config_tags,
507
+ test_skipping_mode: @test_skipping_mode
508
+ )
509
+ .fetch_skippables(test_session)
418
510
  @skippable_tests_fetch_error = skippable_response.error_message unless skippable_response.ok?
419
511
 
512
+ apply_skippable_response(skippable_response)
513
+
514
+ Datadog.logger.debug { "Fetched skippable #{@test_skipping_mode}s: \n #{current_skippables}" }
515
+ Datadog.logger.debug { "Found #{skippables_count} skippable #{@test_skipping_mode}s." }
516
+ Datadog.logger.debug { "ITR correlation ID: #{@correlation_id}" }
517
+
518
+ Utils::Telemetry.inc(skippable_response_metric, skippables_count)
519
+ end
520
+
521
+ def apply_skippable_response(skippable_response)
522
+ set_skippables(
523
+ correlation_id: skippable_response.correlation_id,
524
+ tests: skippable_response.tests,
525
+ suites: skippable_response.suites
526
+ )
527
+ end
528
+
529
+ def set_skippables(correlation_id:, tests:, suites:)
420
530
  @mutex.synchronize do
421
- @correlation_id = skippable_response.correlation_id
422
- @skippable_tests = skippable_response.tests
531
+ @correlation_id = correlation_id
532
+ @skippable_tests = tests
533
+ @skippable_suites = suites
423
534
  end
535
+ end
424
536
 
425
- Datadog.logger.debug { "Fetched skippable tests: \n #{@skippable_tests}" }
426
- Datadog.logger.debug { "Found #{@skippable_tests.count} skippable tests." }
427
- Datadog.logger.debug { "ITR correlation ID: #{@correlation_id}" }
537
+ def current_skippables
538
+ suite_skipping_mode? ? @skippable_suites : @skippable_tests
539
+ end
428
540
 
429
- Utils::Telemetry.inc(Ext::Telemetry::METRIC_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS, @skippable_tests.count)
541
+ def skippable_response_metric
542
+ if skipping_suites?
543
+ Ext::Telemetry::METRIC_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES
544
+ else
545
+ Ext::Telemetry::METRIC_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS
546
+ end
430
547
  end
431
548
 
432
549
  def code_coverage_mode
@@ -21,7 +21,7 @@ module Datadog
21
21
  def valid?
22
22
  valid = true
23
23
 
24
- %i[test_id test_suite_id test_session_id coverage].each do |key|
24
+ %i[test_suite_id test_session_id coverage].each do |key|
25
25
  next unless send(key).nil?
26
26
 
27
27
  Datadog.logger.warn("citestcov event is invalid: [#{key}] is nil. Event: #{self}")
@@ -34,7 +34,7 @@ module Datadog
34
34
  def to_msgpack(packer = nil)
35
35
  packer ||= MessagePack::Packer.new
36
36
 
37
- packer.write_map_header(4)
37
+ packer.write_map_header(test_id.nil? ? 3 : 4)
38
38
 
39
39
  packer.write("test_session_id")
40
40
  packer.write(test_session_id.to_i)
@@ -42,8 +42,10 @@ module Datadog
42
42
  packer.write("test_suite_id")
43
43
  packer.write(test_suite_id.to_i)
44
44
 
45
- packer.write("span_id")
46
- packer.write(test_id.to_i)
45
+ unless test_id.nil?
46
+ packer.write("span_id")
47
+ packer.write(test_id.to_i)
48
+ end
47
49
 
48
50
  files = coverage.keys
49
51
  packer.write("files")
@@ -2,13 +2,16 @@
2
2
 
3
3
  require "set"
4
4
 
5
+ require_relative "../ext/test"
6
+
5
7
  module Datadog
6
8
  module CI
7
9
  module TestImpactAnalysis
8
10
  # No-op implementation used when test impact analysis is disabled.
9
11
  class NullComponent
10
12
  attr_reader :enabled, :skippable_tests_fetch_error, :test_skipping_enabled,
11
- :code_coverage_enabled, :skippable_tests, :correlation_id
13
+ :code_coverage_enabled, :skippable_tests, :skippable_suites, :correlation_id,
14
+ :test_skipping_mode
12
15
 
13
16
  def initialize
14
17
  @enabled = false
@@ -16,7 +19,9 @@ module Datadog
16
19
  @code_coverage_enabled = false
17
20
  @skippable_tests_fetch_error = nil
18
21
  @skippable_tests = Set.new
22
+ @skippable_suites = Set.new
19
23
  @correlation_id = nil
24
+ @test_skipping_mode = Ext::Test::TIATestSkippingMode::TEST
20
25
  end
21
26
 
22
27
  def configure(_remote_configuration = nil, _test_session = nil)
@@ -30,6 +35,10 @@ module Datadog
30
35
  false
31
36
  end
32
37
 
38
+ def skipping_suites?
39
+ false
40
+ end
41
+
33
42
  def code_coverage?
34
43
  false
35
44
  end
@@ -51,6 +60,13 @@ module Datadog
51
60
  nil
52
61
  end
53
62
 
63
+ def on_test_suite_started(_test_suite)
64
+ end
65
+
66
+ def on_test_suite_finished(_test_suite, _context)
67
+ nil
68
+ end
69
+
54
70
  def clear_context_coverage(_context_id)
55
71
  end
56
72
 
@@ -65,10 +81,17 @@ module Datadog
65
81
  false
66
82
  end
67
83
 
84
+ def skippable_suite?(_test_suite_name)
85
+ false
86
+ end
87
+
68
88
  def write_test_session_tags(_test_session, _skipped_tests_count)
69
89
  end
70
90
 
71
- def skippable_tests_count
91
+ def mark_if_suite_skippable(_test_suite)
92
+ end
93
+
94
+ def skippables_count
72
95
  0
73
96
  end
74
97
 
@@ -36,7 +36,7 @@ module Datadog
36
36
 
37
37
  payload.fetch("data", [])
38
38
  .each do |test_data|
39
- next unless test_data["type"] == Ext::Test::ITR_TEST_SKIPPING_MODE
39
+ next unless test_data["type"] == Ext::Test::TIATestSkippingMode::TEST
40
40
 
41
41
  attrs = test_data["attributes"] || {}
42
42
  res << Utils::TestRun.datadog_test_id(attrs["name"], attrs["suite"], attrs["parameters"])
@@ -45,6 +45,20 @@ module Datadog
45
45
  res
46
46
  end
47
47
 
48
+ def suites
49
+ res = Set.new
50
+
51
+ payload.fetch("data", [])
52
+ .each do |test_data|
53
+ next unless test_data["type"] == Ext::Test::TIATestSkippingMode::SUITE
54
+
55
+ suite = (test_data["attributes"] || {})["suite"]
56
+ res << suite unless suite.nil?
57
+ end
58
+
59
+ res
60
+ end
61
+
48
62
  def error_message
49
63
  return nil if ok?
50
64
 
@@ -74,18 +88,19 @@ module Datadog
74
88
  end
75
89
  end
76
90
 
77
- def initialize(dd_env:, api: nil, config_tags: {})
91
+ def initialize(dd_env:, api: nil, config_tags: {}, test_skipping_mode: Ext::Test::TIATestSkippingMode::TEST)
78
92
  @api = api
79
93
  @dd_env = dd_env
80
94
  @config_tags = config_tags
95
+ @test_skipping_mode = test_skipping_mode
81
96
  end
82
97
 
83
- def fetch_skippable_tests(test_session)
98
+ def fetch_skippables(test_session)
84
99
  api = @api
85
100
  return Response.from_http_response(nil) unless api
86
101
 
87
102
  request_payload = payload(test_session)
88
- Datadog.logger.debug("Fetching skippable tests with request: #{request_payload}")
103
+ Datadog.logger.debug("Fetching skippable #{@test_skipping_mode}s with request: #{request_payload}")
89
104
 
90
105
  http_response = api.api_request(
91
106
  path: Ext::Transport::DD_API_SKIPPABLE_TESTS_PATH,
@@ -127,7 +142,7 @@ module Datadog
127
142
  "data" => {
128
143
  "type" => Ext::Transport::DD_API_SKIPPABLE_TESTS_TYPE,
129
144
  "attributes" => {
130
- "test_level" => Ext::Test::ITR_TEST_SKIPPING_MODE,
145
+ "test_level" => @test_skipping_mode,
131
146
  "service" => test_session.service,
132
147
  "env" => @dd_env,
133
148
  "repository_url" => test_session.git_repository_url,
@@ -48,13 +48,13 @@ module Datadog
48
48
 
49
49
  # starting and finishing a test session is required to get the skippable tests response
50
50
  Datadog::CI.start_test_session(total_tests_count: estimated_tests_count)&.finish
51
- skippable_tests_count = test_impact_analysis.skippable_tests_count
51
+ skippables_count = test_impact_analysis.skippables_count
52
52
 
53
53
  log("Estimated tests count: #{estimated_tests_count}")
54
- log("Skippable tests count: #{skippable_tests_count}")
54
+ log("Skippables count: #{skippables_count}")
55
55
  validate_test_impact_analysis_state!
56
56
 
57
- [(skippable_tests_count.to_f / estimated_tests_count).floor(2), 0.99].min || 0.0
57
+ [(skippables_count.to_f / estimated_tests_count).floor(2), 0.99].min || 0.0
58
58
  end
59
59
  end
60
60
  end
@@ -49,7 +49,7 @@ module Datadog
49
49
  tests_hash.each_value.flat_map do |test_configs|
50
50
  test_configs.map do |test_config|
51
51
  {
52
- "type" => Ext::Test::ITR_TEST_SKIPPING_MODE,
52
+ "type" => Ext::Test::DEFAULT_TIA_TEST_SKIPPING_MODE,
53
53
  "attributes" => {
54
54
  "suite" => test_config["suite"],
55
55
  "name" => test_config["name"],
@@ -111,6 +111,26 @@ module Datadog
111
111
  end
112
112
  end
113
113
 
114
+ # @internal
115
+ def datadog_skip_reason
116
+ get_tag(Ext::Test::TAG_SKIP_REASON)
117
+ end
118
+
119
+ # @internal
120
+ def should_skip?
121
+ skipped_by_test_impact_analysis?
122
+ end
123
+
124
+ # @internal
125
+ def skipped_by_test_impact_analysis?
126
+ get_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR) == "true"
127
+ end
128
+
129
+ # @internal
130
+ def itr_unskippable?
131
+ get_tag(Ext::Test::TAG_ITR_UNSKIPPABLE) == "true"
132
+ end
133
+
114
134
  private
115
135
 
116
136
  def set_status_from_stats!
@@ -272,6 +272,7 @@ module Datadog
272
272
 
273
273
  def on_test_suite_started(test_suite)
274
274
  set_codeowners(test_suite)
275
+ test_impact_analysis.on_test_suite_started(test_suite)
275
276
  end
276
277
 
277
278
  def on_test_started(test)
@@ -318,6 +319,7 @@ module Datadog
318
319
  end
319
320
 
320
321
  def on_test_suite_finished(test_suite)
322
+ test_impact_analysis.on_test_suite_finished(test_suite, maybe_remote_context)
321
323
  Telemetry.event_finished(test_suite)
322
324
  end
323
325
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../ext/test"
3
4
  require_relative "../git/local_repository"
4
5
 
5
6
  module Datadog
@@ -13,6 +14,17 @@ module Datadog
13
14
  def self.service_name_provided_by_user?
14
15
  !!Datadog.configuration.service_without_fallback
15
16
  end
17
+
18
+ def self.normalize_tia_test_skipping_mode(mode)
19
+ return mode if Ext::Test::TIATestSkippingMode::ALL.include?(mode)
20
+
21
+ Datadog.logger.warn(
22
+ "Invalid Test Impact Analysis skipping mode #{mode.inspect}. " \
23
+ "Falling back to #{Ext::Test::TIATestSkippingMode::TEST.inspect}."
24
+ )
25
+
26
+ Ext::Test::TIATestSkippingMode::TEST
27
+ end
16
28
  end
17
29
  end
18
30
  end
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 33
7
+ MINOR = 34
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.33.0
4
+ version: 1.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.