datadog-ci 1.33.0 → 1.35.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -2
  3. data/lib/datadog/ci/configuration/components.rb +2 -0
  4. data/lib/datadog/ci/configuration/settings.rb +11 -0
  5. data/lib/datadog/ci/contrib/cucumber/formatter.rb +51 -1
  6. data/lib/datadog/ci/contrib/cucumber/instrumentation.rb +5 -0
  7. data/lib/datadog/ci/contrib/minitest/helpers.rb +54 -6
  8. data/lib/datadog/ci/contrib/minitest/runnable.rb +5 -0
  9. data/lib/datadog/ci/contrib/minitest/runnable_minitest_6.rb +5 -0
  10. data/lib/datadog/ci/contrib/minitest/test.rb +21 -27
  11. data/lib/datadog/ci/contrib/patcher.rb +0 -4
  12. data/lib/datadog/ci/contrib/rspec/anonymous_example_name.rb +515 -0
  13. data/lib/datadog/ci/contrib/rspec/example.rb +23 -5
  14. data/lib/datadog/ci/contrib/rspec/example_group.rb +11 -1
  15. data/lib/datadog/ci/contrib/simplecov/result_extractor.rb +9 -1
  16. data/lib/datadog/ci/ext/settings.rb +1 -0
  17. data/lib/datadog/ci/ext/telemetry.rb +1 -0
  18. data/lib/datadog/ci/ext/test.rb +8 -1
  19. data/lib/datadog/ci/remote/library_settings_client.rb +3 -2
  20. data/lib/datadog/ci/source_code/method_inspect.rb +0 -3
  21. data/lib/datadog/ci/test_discovery/component.rb +4 -0
  22. data/lib/datadog/ci/test_impact_analysis/component.rb +176 -59
  23. data/lib/datadog/ci/test_impact_analysis/coverage/event.rb +6 -4
  24. data/lib/datadog/ci/test_impact_analysis/null_component.rb +25 -2
  25. data/lib/datadog/ci/test_impact_analysis/skippable.rb +20 -5
  26. data/lib/datadog/ci/test_impact_analysis/skippable_percentage/estimator.rb +3 -3
  27. data/lib/datadog/ci/test_optimization_cache/readers/legacy.rb +1 -1
  28. data/lib/datadog/ci/test_suite.rb +20 -0
  29. data/lib/datadog/ci/test_tracing/component.rb +10 -0
  30. data/lib/datadog/ci/test_tracing/deprecated_total_coverage_metric.rb +11 -1
  31. data/lib/datadog/ci/utils/configuration.rb +12 -0
  32. data/lib/datadog/ci/utils/test_name.rb +112 -0
  33. data/lib/datadog/ci/version.rb +1 -1
  34. metadata +3 -1
@@ -0,0 +1,515 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Contrib
6
+ module RSpec
7
+ module AnonymousExampleName
8
+ ISEQ_SIMPLE_DATA_FORMAT = "YARVInstructionSequence/SimpleDataFormat"
9
+
10
+ SELF_VALUE = :__datadog_rspec_self__
11
+ EMPTY_ARGUMENTS = [].freeze
12
+ MINIMUM_RUBY_VERSION = Gem::Version.new("3.2")
13
+
14
+ EXPECTATION_METHODS = {
15
+ to: "is expected to",
16
+ not_to: "is expected not to",
17
+ to_not: "is expected not to"
18
+ }.freeze
19
+ SHOULD_METHODS = {
20
+ should: "is expected to",
21
+ should_not: "is expected not to"
22
+ }.freeze
23
+ OPERATOR_METHODS = %i[== != === =~ !~ < <= > >= + - * / %].each_with_object({}) { |method_name, methods|
24
+ methods[method_name] = true
25
+ }.freeze
26
+ MATCHER_CHAIN_METHODS = %i[
27
+ and
28
+ at_least
29
+ at_most
30
+ by
31
+ by_at_least
32
+ by_at_most
33
+ exactly
34
+ from
35
+ of
36
+ once
37
+ or
38
+ times
39
+ to
40
+ twice
41
+ with
42
+ within
43
+ ].each_with_object({}) { |method_name, methods|
44
+ methods[method_name] = true
45
+ }.freeze
46
+ PRETTY_MATCHER_PREFIXES = %w[be_ have_ raise_ respond_ yield_ start_ end_].freeze
47
+ PRETTY_MATCHER_METHODS = %i[
48
+ be
49
+ change
50
+ contain_exactly
51
+ eq
52
+ eql
53
+ equal
54
+ exist
55
+ include
56
+ match
57
+ output
58
+ raise_error
59
+ satisfy
60
+ throw_symbol
61
+ ].each_with_object({}) { |method_name, methods|
62
+ methods[method_name] = true
63
+ }.freeze
64
+ MAX_RENDERED_VALUE_LENGTH = 80
65
+ MAX_RENDERED_NAME_LENGTH = 180
66
+
67
+ def self.supported?
68
+ return false unless defined?(RubyVM::InstructionSequence)
69
+ return false unless RUBY_ENGINE == "ruby"
70
+
71
+ Gem::Version.new(RUBY_VERSION) >= MINIMUM_RUBY_VERSION
72
+ end
73
+
74
+ def self.call(target)
75
+ return nil if target.nil?
76
+ return nil unless supported?
77
+
78
+ iseq = iseq_for(target)
79
+ return nil unless iseq
80
+
81
+ rendered_name = render_iseq(iseq)
82
+ trim(rendered_name, MAX_RENDERED_NAME_LENGTH) if rendered_name
83
+ rescue => e
84
+ warn_anonymous_example_name_error(e)
85
+ nil
86
+ end
87
+
88
+ def self.iseq_for(target)
89
+ return target if target.is_a?(RubyVM::InstructionSequence)
90
+
91
+ RubyVM::InstructionSequence.of(target)
92
+ end
93
+ private_class_method :iseq_for
94
+
95
+ def self.render_iseq(iseq)
96
+ body = iseq.to_a[13]
97
+ return nil unless body.is_a?(Array)
98
+
99
+ # Most anonymous RSpec examples end with `<expectation>.to <matcher>`.
100
+ # Render only that known shape by walking backwards from the final matcher call:
101
+ # it avoids simulating unrelated subject code, so dynamic values do not
102
+ # leak into the name and dangerous user code is not evaluated.
103
+ expectation_call_index = final_expectation_call_index(body)
104
+ return nil unless expectation_call_index
105
+
106
+ call_data = body[expectation_call_index][1]
107
+ return nil unless call_data[:orig_argc].to_i == 1
108
+
109
+ matcher_expression = render_expression_ending_at(body, expectation_call_index - 1)
110
+ return nil unless matcher_expression
111
+
112
+ method_name = call_data[:mid]
113
+ return nil if EXPECTATION_METHODS.key?(method_name) && !expectation_receiver_at?(body, matcher_expression[1])
114
+
115
+ name_prefix = EXPECTATION_METHODS[method_name] || SHOULD_METHODS[method_name]
116
+ "#{name_prefix} #{matcher_expression[0]}"
117
+ end
118
+ private_class_method :render_iseq
119
+
120
+ def self.final_expectation_call_index(body)
121
+ index = body.length - 1
122
+
123
+ while index >= 0
124
+ entry = body[index]
125
+
126
+ if entry.is_a?(Array)
127
+ call_data = entry[1]
128
+ if call_data.is_a?(Hash)
129
+ method_name = call_data[:mid]
130
+ return index if EXPECTATION_METHODS.key?(method_name) || SHOULD_METHODS.key?(method_name)
131
+ end
132
+ end
133
+
134
+ index -= 1
135
+ end
136
+
137
+ nil
138
+ end
139
+ private_class_method :final_expectation_call_index
140
+
141
+ def self.expectation_receiver_at?(body, index)
142
+ index = previous_instruction_index(body, index)
143
+ return false unless index
144
+
145
+ instruction = body[index]
146
+ call_data = instruction[1]
147
+ return false unless call_data.is_a?(Hash)
148
+
149
+ method_name = call_data[:mid]
150
+ method_name == :expect || method_name == :is_expected
151
+ end
152
+ private_class_method :expectation_receiver_at?
153
+
154
+ # Render the expression that produced the stack value at `index`.
155
+ #
156
+ # YARV bytecode is stack-based: matcher arguments are pushed before the
157
+ # final `to`/`not_to` call consumes them. We walk backwards from the
158
+ # producer instruction, render only shapes we understand, and return the
159
+ # previous instruction index so callers can continue rendering earlier
160
+ # stack values without evaluating user code.
161
+ def self.render_expression_ending_at(body, index)
162
+ return nil unless index
163
+
164
+ index = previous_instruction_index(body, index)
165
+ return nil unless index
166
+
167
+ instruction = body[index]
168
+ opcode = instruction[0]
169
+
170
+ case opcode
171
+ when :putself
172
+ [SELF_VALUE, index - 1]
173
+ when :putnil
174
+ ["nil", index - 1]
175
+ when :putobject, :putchilledstring, :putstring
176
+ [render_literal(instruction[1]), index - 1]
177
+ when :duparray
178
+ [render_array_literal(instruction[1]), index - 1]
179
+ when :duphash
180
+ [render_hash_literal(instruction[1]), index - 1]
181
+ when :newarray
182
+ render_new_array_expression(body, index)
183
+ when :newhash
184
+ render_new_hash_expression(body, index)
185
+ when :opt_getconstant_path
186
+ [render_constant_path(instruction[1]), index - 1]
187
+ when :pop
188
+ render_optimized_new_expression(body, index)
189
+ when :send, :opt_send_without_block, :opt_plus, :opt_minus, :opt_mult, :opt_div,
190
+ :opt_mod, :opt_eq, :opt_neq, :opt_lt, :opt_le, :opt_gt, :opt_ge
191
+ render_send_expression(body, index, instruction)
192
+ else
193
+ render_special_expression(index, instruction)
194
+ end
195
+ end
196
+ private_class_method :render_expression_ending_at
197
+
198
+ # `newarray` consumes N already-pushed stack values. Render those values
199
+ # backwards, then restore source order so matcher names read like Ruby.
200
+ def self.render_new_array_expression(body, index)
201
+ expression = render_expression_list_ending_at(body, index - 1, body[index][1].to_i)
202
+ return nil unless expression
203
+
204
+ [render_array_literal(expression[0]), expression[1]]
205
+ end
206
+ private_class_method :render_new_array_expression
207
+
208
+ # `newhash` consumes alternating key/value stack entries. We render the
209
+ # pairs instead of inspecting a runtime Hash, keeping generated names
210
+ # deterministic even when the original values are local variables.
211
+ def self.render_new_hash_expression(body, index)
212
+ expression = render_expression_list_ending_at(body, index - 1, body[index][1].to_i)
213
+ return nil unless expression
214
+
215
+ values = expression[0]
216
+ pairs = values.each_slice(2).map { |key, value| "#{key} => #{value}" }
217
+ ["{#{pairs.join(", ")}}", expression[1]]
218
+ end
219
+ private_class_method :render_new_hash_expression
220
+
221
+ def self.render_optimized_new_expression(body, index)
222
+ # Ruby 4 compiles zero-argument `Class.new` with `opt_new` followed by
223
+ # constructor bookkeeping and a final `pop`. Collapse that whole shape
224
+ # back to the receiver class so `Object.new` renders as stable `Object`.
225
+ swap_index = previous_instruction_index(body, index - 1)
226
+ return nil unless swap_index
227
+ return nil unless body[swap_index][0] == :swap
228
+
229
+ receiver_index = optimized_new_receiver_index(body, swap_index - 1)
230
+ return nil unless receiver_index
231
+
232
+ receiver_expression = render_expression_ending_at(body, receiver_index)
233
+ return nil unless receiver_expression
234
+
235
+ [render_send(receiver_expression[0], :new, EMPTY_ARGUMENTS, nil), receiver_expression[1]]
236
+ end
237
+ private_class_method :render_optimized_new_expression
238
+
239
+ # Ruby 4's optimized constructor bytecode places the receiver before
240
+ # `putnil`, `swap`, and `opt_new`. Find that receiver so the outer `pop`
241
+ # handler can render `Object.new` as the stable class name `Object`.
242
+ def self.optimized_new_receiver_index(body, index)
243
+ while index >= 0
244
+ entry = body[index]
245
+
246
+ if entry.is_a?(Array) && entry[0] == :opt_new
247
+ call_data = entry[1]
248
+ if call_data.is_a?(Hash) && call_data[:mid] == :new && call_data[:orig_argc].to_i.zero?
249
+ swap_index = previous_instruction_index(body, index - 1)
250
+ return nil unless swap_index
251
+ return nil unless body[swap_index][0] == :swap
252
+
253
+ nil_index = previous_instruction_index(body, swap_index - 1)
254
+ return nil unless nil_index
255
+ return nil unless body[nil_index][0] == :putnil
256
+
257
+ return previous_instruction_index(body, nil_index - 1)
258
+ end
259
+ end
260
+
261
+ index -= 1
262
+ end
263
+
264
+ nil
265
+ end
266
+ private_class_method :optimized_new_receiver_index
267
+
268
+ # Method calls consume arguments first and the receiver last. Render that
269
+ # stack shape backwards, then format the send as matcher-style text when
270
+ # it belongs to RSpec's expectation DSL.
271
+ def self.render_send_expression(body, index, instruction)
272
+ call_data = instruction[1]
273
+ return nil unless call_data.is_a?(Hash)
274
+
275
+ arguments_expression = render_expression_list_ending_at(body, index - 1, call_data[:orig_argc].to_i)
276
+ return nil unless arguments_expression
277
+
278
+ arguments = arguments_expression[0]
279
+ receiver_end_index = arguments_expression[1]
280
+
281
+ receiver_index = previous_instruction_index(body, receiver_end_index)
282
+ return nil unless receiver_index
283
+
284
+ receiver_instruction = body[receiver_index]
285
+ if receiver_instruction[0] == :putself
286
+ receiver = SELF_VALUE
287
+ previous_index = receiver_index - 1
288
+ else
289
+ receiver_expression = render_expression_ending_at(body, receiver_index)
290
+ return nil unless receiver_expression
291
+
292
+ receiver = receiver_expression[0]
293
+ previous_index = receiver_expression[1]
294
+ end
295
+
296
+ block_iseq = instruction[2] if iseq_array?(instruction[2])
297
+ rendered_send = render_send(receiver, call_data[:mid], arguments, block_iseq)
298
+
299
+ [rendered_send, previous_index]
300
+ end
301
+ private_class_method :render_send_expression
302
+
303
+ # Render `count` adjacent stack expressions ending at `index`.
304
+ # Arguments appear on the stack left-to-right, but because we scan from
305
+ # the end, each rendered value is written back into its original slot.
306
+ def self.render_expression_list_ending_at(body, index, count)
307
+ return [EMPTY_ARGUMENTS, index] if count <= 0
308
+
309
+ arguments = Array.new(count)
310
+ argument_index = count - 1
311
+
312
+ while argument_index >= 0
313
+ expression = render_expression_ending_at(body, index)
314
+ return nil unless expression
315
+
316
+ arguments[argument_index] = expression[0]
317
+ index = expression[1]
318
+ argument_index -= 1
319
+ end
320
+
321
+ [arguments, index]
322
+ end
323
+ private_class_method :render_expression_list_ending_at
324
+
325
+ # Instruction sequence bodies also contain labels and events. Only array
326
+ # entries are executable instructions for the subset we render here.
327
+ def self.previous_instruction_index(body, index)
328
+ while index >= 0
329
+ return index if body[index].is_a?(Array)
330
+
331
+ index -= 1
332
+ end
333
+
334
+ nil
335
+ end
336
+ private_class_method :previous_instruction_index
337
+
338
+ # Handle compact Ruby opcodes that are common in tiny matcher arguments.
339
+ # Locals are intentionally rendered as `local`; reading their runtime
340
+ # values would make names unstable and could execute user code.
341
+ def self.render_special_expression(index, instruction)
342
+ opcode = instruction[0].to_s
343
+
344
+ if opcode.start_with?("putobject_INT2FIX_")
345
+ [opcode.delete_prefix("putobject_INT2FIX_").delete_suffix("_"), index - 1]
346
+ elsif opcode.start_with?("getlocal")
347
+ ["local", index - 1]
348
+ end
349
+ end
350
+ private_class_method :render_special_expression
351
+
352
+ # Convert a rendered send into the text users expect from an RSpec
353
+ # generated description. For constructor calls on constants, keep only
354
+ # the class name so `Object.new` does not introduce object identity.
355
+ def self.render_send(receiver, method_name, arguments, block_iseq)
356
+ if receiver == SELF_VALUE && arguments.empty? && !block_iseq
357
+ return render_method_name(receiver, method_name, arguments, block_iseq)
358
+ end
359
+
360
+ return receiver if method_name == :new && arguments.empty? && constant_name?(receiver)
361
+
362
+ render_method_call(receiver, method_name, arguments, block_iseq)
363
+ end
364
+ private_class_method :render_send
365
+
366
+ # Format non-trivial sends. Matcher calls and matcher chains use RSpec's
367
+ # human-readable style (`include "x"`, `change by 1`); ordinary nested
368
+ # sends keep Ruby-ish receiver syntax (`local.to_s`) for clarity.
369
+ def self.render_method_call(receiver, method_name, arguments, block_iseq)
370
+ matcher_chain = MATCHER_CHAIN_METHODS.key?(method_name)
371
+ method_text = render_method_name(receiver, method_name, arguments, block_iseq)
372
+ argument_text = arguments.join(", ")
373
+ receiver_text = (receiver == SELF_VALUE) ? "self" : receiver.to_s
374
+
375
+ if OPERATOR_METHODS.key?(method_name)
376
+ return "#{receiver_text} #{method_name} #{argument_text}".strip
377
+ end
378
+
379
+ if receiver == SELF_VALUE
380
+ return argument_text.empty? ? method_text : "#{method_text} #{argument_text}"
381
+ end
382
+
383
+ if matcher_chain
384
+ return argument_text.empty? ? "#{receiver} #{method_text}" : "#{receiver} #{method_text} #{argument_text}"
385
+ end
386
+
387
+ argument_suffix = argument_text.empty? ? "" : "(#{argument_text})"
388
+ "#{receiver_text}.#{method_name}#{argument_suffix}"
389
+ end
390
+ private_class_method :render_method_call
391
+
392
+ # RSpec generated descriptions turn matcher-like method names into prose
393
+ # only when the send is part of matcher DSL text. Keep ordinary nested
394
+ # Ruby calls (`local.to_s`) untouched.
395
+ def self.render_method_name(receiver, method_name, arguments, block_iseq)
396
+ method_text = method_name.to_s
397
+ return method_text unless humanize_method_name?(receiver, method_name, method_text, arguments, block_iseq)
398
+
399
+ method_text.tr("_", " ")
400
+ end
401
+ private_class_method :render_method_name
402
+
403
+ def self.humanize_method_name?(receiver, method_name, method_text, arguments, block_iseq)
404
+ return false unless receiver == SELF_VALUE || MATCHER_CHAIN_METHODS.key?(method_name) || block_iseq
405
+
406
+ pretty_matcher_method?(method_name, method_text) || !arguments.empty? || block_iseq
407
+ end
408
+ private_class_method :humanize_method_name?
409
+
410
+ # RSpec turns many matcher method names into words in generated example
411
+ # descriptions. Mirror that only for known matcher methods/prefixes.
412
+ def self.pretty_matcher_method?(method_name, method_text)
413
+ PRETTY_MATCHER_METHODS.key?(method_name) ||
414
+ PRETTY_MATCHER_PREFIXES.any? { |prefix| method_text.start_with?(prefix) }
415
+ end
416
+ private_class_method :pretty_matcher_method?
417
+
418
+ # Literal operands are embedded in bytecode and safe to read. For any
419
+ # object-like value, use the class name instead of `inspect` so memory
420
+ # addresses cannot leak into test identities.
421
+ def self.render_literal(value)
422
+ rendered_value =
423
+ case value
424
+ when nil
425
+ "nil"
426
+ when true
427
+ "true"
428
+ when false
429
+ "false"
430
+ when Symbol
431
+ value.inspect
432
+ when String
433
+ value.inspect
434
+ when Numeric
435
+ value.to_s
436
+ when Regexp
437
+ value.inspect
438
+ when Array
439
+ render_array_literal(value)
440
+ when Hash
441
+ render_hash_literal(value)
442
+ else
443
+ value.class.name || "value"
444
+ end
445
+
446
+ trim(rendered_value, MAX_RENDERED_VALUE_LENGTH)
447
+ end
448
+ private_class_method :render_literal
449
+
450
+ # Keep array literals readable while recursively applying the same stable
451
+ # rendering rules to their elements.
452
+ def self.render_array_literal(value)
453
+ return "[]" unless value.is_a?(Array)
454
+
455
+ "[#{value.map { |element| render_literal(element) }.join(", ")}]"
456
+ end
457
+ private_class_method :render_array_literal
458
+
459
+ # Sort hash keys so equivalent literal hashes render consistently across
460
+ # Ruby versions and construction paths.
461
+ def self.render_hash_literal(value)
462
+ return "{}" unless value.is_a?(Hash)
463
+
464
+ pairs = value.keys.sort_by(&:to_s).map do |key|
465
+ "#{render_literal(key)} => #{render_literal(value[key])}"
466
+ end
467
+
468
+ "{#{pairs.join(", ")}}"
469
+ end
470
+ private_class_method :render_hash_literal
471
+
472
+ # `opt_getconstant_path` stores constants as path parts. Join them into a
473
+ # Ruby-looking constant name without resolving the constant at runtime.
474
+ def self.render_constant_path(value)
475
+ return "constant" unless value.is_a?(Array)
476
+
477
+ value.compact.join("::")
478
+ end
479
+ private_class_method :render_constant_path
480
+
481
+ # Used when collapsing zero-argument constructor calls; only collapse
482
+ # real constant paths, not arbitrary receiver text.
483
+ def self.constant_name?(value)
484
+ value.is_a?(String) && value.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)*\z/)
485
+ end
486
+ private_class_method :constant_name?
487
+
488
+ # Block matchers carry the block body as an embedded instruction
489
+ # sequence. Its presence changes matcher wording, but rendering the
490
+ # block body would risk pulling subject code into the name.
491
+ def self.iseq_array?(value)
492
+ value.is_a?(Array) && value[0] == ISEQ_SIMPLE_DATA_FORMAT
493
+ end
494
+ private_class_method :iseq_array?
495
+
496
+ # Bound rendered values and names so unusually large literals do not
497
+ # create oversized span names.
498
+ def self.trim(value, max_length)
499
+ return value if value.length <= max_length
500
+
501
+ "#{value[0, max_length - 3]}..."
502
+ end
503
+ private_class_method :trim
504
+
505
+ def self.warn_anonymous_example_name_error(error)
506
+ Datadog.logger.warn { "Unable to compute RSpec anonymous example name: #{error.class}: #{error.message}" }
507
+
508
+ nil
509
+ end
510
+ private_class_method :warn_anonymous_example_name_error
511
+ end
512
+ end
513
+ end
514
+ end
515
+ end
@@ -7,6 +7,7 @@ require_relative "../../source_code/path_filter"
7
7
  require_relative "../../utils/bundle"
8
8
  require_relative "../../utils/test_run"
9
9
  require_relative "../instrumentation"
10
+ require_relative "anonymous_example_name"
10
11
  require_relative "ext"
11
12
 
12
13
  module Datadog
@@ -110,11 +111,7 @@ module Datadog
110
111
  def datadog_test_name
111
112
  return @datadog_test_name if defined?(@datadog_test_name)
112
113
 
113
- test_name = full_description.strip
114
- if metadata[:description].empty?
115
- # for unnamed it blocks this appends something like "example at ./spec/some_spec.rb:10"
116
- test_name << " #{description}"
117
- end
114
+ test_name = datadog_unnamed_example? ? datadog_unnamed_example_name : full_description.strip
118
115
 
119
116
  # remove example group description from test name to avoid duplication
120
117
  test_name = test_name.sub(datadog_test_suite_description, "").strip
@@ -266,6 +263,27 @@ module Datadog
266
263
  end
267
264
  end
268
265
 
266
+ # ============================================
267
+ # Test name helpers
268
+ # ============================================
269
+
270
+ def datadog_unnamed_example?
271
+ # @type var description_args: Array[untyped]
272
+ description_args = Array(metadata[:description_args])
273
+ description_args.empty? || description_args.all? { |description_arg| description_arg.to_s.strip.empty? }
274
+ end
275
+
276
+ def datadog_unnamed_example_name
277
+ example_name = AnonymousExampleName.call(datadog_anonymous_example_block)
278
+ example_name ||= "anonymous example"
279
+
280
+ "#{metadata[:example_group][:full_description]} #{example_name}"
281
+ end
282
+
283
+ def datadog_anonymous_example_block
284
+ metadata[:block] || @example_block
285
+ end
286
+
269
287
  # ============================================
270
288
  # Source location resolution
271
289
  # ============================================
@@ -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
@@ -22,7 +22,15 @@ module Datadog
22
22
  ::SimpleCov::ResultAdapter.call(::Coverage.peek_result)
23
23
  )
24
24
 
25
- ::SimpleCov::Result.new(add_not_loaded_files(result))
25
+ # SimpleCov 1.0 changed add_not_loaded_files to return a
26
+ # [coverage, not_loaded_files] tuple instead of a coverage hash.
27
+ processed = add_not_loaded_files(result)
28
+ if processed.is_a?(::Array)
29
+ coverage, not_loaded_files = processed
30
+ ::SimpleCov::Result.new(coverage, not_loaded_files: not_loaded_files)
31
+ else
32
+ ::SimpleCov::Result.new(processed)
33
+ end
26
34
  end
27
35
 
28
36
  def datadog_configuration
@@ -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,