datadog-ci 1.34.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2adf2fb252fdff7c6d0e1e73c2b6ade74c53bc5e8a238d447793da6833837c0
4
- data.tar.gz: a49b2d09ae48bb2dc90c9846c6aae128a05d75353a2e7dc9df248960f165f556
3
+ metadata.gz: f1aa81f9987d876f143abe3a9b7721e1a5b35178c5c854dcdaf79b872c5a0c9e
4
+ data.tar.gz: 261ea8b1b233ac66c0ba7b417018fe477afb05a274ed6607b6bf3c235c58c3ff
5
5
  SHA512:
6
- metadata.gz: 75d99396decbd20bb93cc2e061295be1e55c033b41980216bc0bb9229fd36269e7ad8bd5ad1104203ca10a32bed668aa65f41d01ddd5c8ebef34d206397a7c25
7
- data.tar.gz: 3a0efeaaad0042cd836c51a7232a0348b377acf959bc6e10670e01ed5d7ea863caf60853142cb7e2d563da1c64ef1dadee5c3735a69bf482dfdb7b611239767d
6
+ metadata.gz: 05aed65898a6b065f99e6a9fd2aab34023fd3ac77aaacc4146d39d8a1e6653ad5cb6d970afc5e6d590a4ddf6eec3f54704089a5ea49d7bf2f614d27d1572ed91
7
+ data.tar.gz: f89daf423afed1bd0ac7c227a28c4a980b92916fcf03a078a9278eb880e059c9fb40f8dc3b0887645e8a806f2e879e70bf23eaaa7500cd3e6e6e2d4b518a449f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.35.0] - 2026-07-23
4
+
3
5
  ## [1.34.0] - 2026-06-25
4
6
 
5
7
  ### Added
@@ -665,7 +667,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
665
667
 
666
668
  - Ruby versions < 2.7 no longer supported ([#8][])
667
669
 
668
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...main
670
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.35.0...main
671
+ [1.35.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...v1.35.0
669
672
  [1.34.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.33.0...v1.34.0
670
673
  [1.33.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.32.0...v1.33.0
671
674
  [1.32.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.31.0...v1.32.0
@@ -81,11 +81,14 @@ module Datadog
81
81
  end
82
82
 
83
83
  def self.extract_runnable_source_location(klass, method_name)
84
- source_location = extract_source_location_from_class(klass)
85
- if source_location.nil? || source_location.empty?
86
- return klass.instance_method(method_name).source_location
84
+ method_source_location = extract_source_location_from_method(klass, method_name)
85
+ klass_source_location = extract_source_location_from_class(klass)
86
+
87
+ if defined?(::Minitest::Spec) && klass&.ancestors&.include?(::Minitest::Spec)
88
+ method_source_location || klass_source_location
89
+ else
90
+ klass_source_location || method_source_location
87
91
  end
88
- source_location
89
92
  end
90
93
 
91
94
  def self.skip_test_suite(test_suite)
@@ -102,9 +105,21 @@ module Datadog
102
105
  end
103
106
 
104
107
  def self.extract_source_location_from_class(klass)
105
- return [] if klass.nil? || klass.name.nil?
108
+ return nil if klass.nil? || klass.name.nil?
109
+
110
+ empty_source_location_to_nil(SourceCode::ConstantResolver.safely_get_const_source_location(klass.name))
111
+ end
106
112
 
107
- SourceCode::ConstantResolver.safely_get_const_source_location(klass.name) || []
113
+ def self.extract_source_location_from_method(klass, method_name)
114
+ return nil if klass.nil?
115
+ return nil if method_name.nil? || method_name.empty?
116
+
117
+ empty_source_location_to_nil(klass.instance_method(method_name).source_location)
118
+ end
119
+
120
+ def self.empty_source_location_to_nil(source_location)
121
+ return nil if source_location.nil? || source_location.empty?
122
+ source_location
108
123
  end
109
124
  end
110
125
  end
@@ -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
  # ============================================
@@ -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
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "json"
5
5
  require_relative "../ext/test"
6
6
  require_relative "../ext/test_discovery"
7
+ require_relative "../utils/test_name"
7
8
 
8
9
  module Datadog
9
10
  module CI
@@ -76,6 +77,9 @@ module Datadog
76
77
  end
77
78
 
78
79
  def record_test(name:, suite:, module_name:, parameters:, source_file:)
80
+ name = name.nil? ? nil : Utils::TestName.normalize(name)
81
+ suite = suite.nil? ? nil : Utils::TestName.normalize(suite)
82
+
79
83
  test_info = {
80
84
  "name" => name,
81
85
  "suite" => suite,
@@ -16,6 +16,7 @@ require_relative "../ext/test"
16
16
  require_relative "../git/local_repository"
17
17
  require_relative "../utils/file_storage"
18
18
  require_relative "../utils/stateful"
19
+ require_relative "../utils/test_name"
19
20
 
20
21
  require_relative "../worker"
21
22
 
@@ -109,6 +110,7 @@ module Datadog
109
110
  def start_test_suite(test_suite_name, service: nil, tags: {})
110
111
  return skip_tracing unless test_suite_level_visibility_enabled
111
112
 
113
+ test_suite_name = Utils::TestName.normalize(test_suite_name)
112
114
  context = @local_test_suites_mode ? @context : maybe_remote_context
113
115
 
114
116
  test_suite = context.start_test_suite(test_suite_name, service: service, tags: tags)
@@ -117,6 +119,9 @@ module Datadog
117
119
  end
118
120
 
119
121
  def trace_test(test_name, test_suite_name, service: nil, tags: {}, &block)
122
+ test_name = Utils::TestName.normalize(test_name)
123
+ test_suite_name = Utils::TestName.normalize(test_suite_name)
124
+
120
125
  test_suite = active_test_suite(test_suite_name)
121
126
  tags[Ext::Test::TAG_SUITE] ||= test_suite_name
122
127
 
@@ -165,6 +170,8 @@ module Datadog
165
170
  # we return the single active test suite because most of the time there is only one test suite running
166
171
  return single_active_test_suite if test_suite_name.nil?
167
172
 
173
+ test_suite_name = Utils::TestName.normalize(test_suite_name)
174
+
168
175
  # when fetching test_suite to use as test's context, try local context instance first
169
176
  local_test_suite = @context.active_test_suite(test_suite_name)
170
177
  return local_test_suite if local_test_suite
@@ -194,6 +201,7 @@ module Datadog
194
201
  end
195
202
 
196
203
  def deactivate_test_suite(test_suite_name)
204
+ test_suite_name = Utils::TestName.normalize(test_suite_name)
197
205
  test_suite = active_test_suite(test_suite_name)
198
206
  on_test_suite_finished(test_suite) if test_suite
199
207
 
@@ -12,7 +12,7 @@ module Datadog
12
12
  return
13
13
  end
14
14
 
15
- unless ::SimpleCov.running
15
+ unless simplecov_running?
16
16
  Datadog.logger.debug("SimpleCov is not running, skipping code coverage extraction")
17
17
  return
18
18
  end
@@ -30,6 +30,16 @@ module Datadog
30
30
 
31
31
  test_session.set_tag(Ext::Test::TAG_CODE_COVERAGE_LINES_PCT, result.covered_percent)
32
32
  end
33
+
34
+ # SimpleCov 1.0 removed the `running` accessor and relies on Ruby's
35
+ # Coverage.running? instead.
36
+ def self.simplecov_running?
37
+ return ::SimpleCov.running if ::SimpleCov.respond_to?(:running)
38
+ return false unless defined?(::Coverage)
39
+
40
+ ::Coverage.running?
41
+ end
42
+ private_class_method :simplecov_running?
33
43
  end
34
44
  end
35
45
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Utils
6
+ module TestName
7
+ MONTH = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
8
+ WEEKDAY = "(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
9
+ CONSTANT_NAME = "[A-Z]\\w*(?:::[A-Z]\\w*)*"
10
+
11
+ DATE_INSPECT_PATTERN = /#<Date(?::[^>\n]*)?>/.freeze
12
+ TIME_INSPECT_PATTERN = /#<(?:DateTime|Time)(?::[^>\n]*)?>/.freeze
13
+ CLASS_INSPECT_PATTERN = /#<Class:(#{CONSTANT_NAME}|0x[0-9a-fA-F]+)(?:\s[^>\n]*)?>/.freeze
14
+ NAMED_OBJECT_INSPECT_PATTERN = /#<(#{CONSTANT_NAME})(?::[^>\n]*)?>/.freeze
15
+ OBJECT_INSPECT_PATTERN = /#<[^>\n]+>/.freeze
16
+ ACTIVE_RECORD_CLASS_INSPECT_PATTERN = /
17
+ \b(#{CONSTANT_NAME})
18
+ (?:
19
+ \s\(call\s'\1\.connection'\sto\ establish\ a\ connection\)
20
+ |
21
+ \(
22
+ (?:\.\.\.\s*)?[a-z_][a-z0-9_]*:\s[^,)\n]*
23
+ (?:
24
+ ,\s(?:\.\.\.\s*)?[a-z_][a-z0-9_]*:\s[^,)\n]*
25
+ )*
26
+ \)
27
+ )
28
+ /x.freeze
29
+
30
+ TIME_PATTERN = /
31
+ \b
32
+ (?:
33
+ \d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?
34
+ (?:\s?(?:Z|UTC|[A-Z]{2,5}|[+-]\d{2}:?\d{2}))?
35
+ |
36
+ #{WEEKDAY},?\s+\d{1,2}\s+#{MONTH}\s+\d{4}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?
37
+ (?:\s+(?:UTC|[A-Z]{2,5}|[+-]\d{2}:?\d{2}))?
38
+ )
39
+ \b
40
+ /x.freeze
41
+ DATE_PATTERN = /
42
+ \b
43
+ (?:
44
+ \d{4}-\d{2}-\d{2}
45
+ |
46
+ #{WEEKDAY},?\s+\d{1,2}\s+#{MONTH}\s+\d{4}
47
+ )
48
+ \b
49
+ /x.freeze
50
+ RANGE_PATTERN = /
51
+ (^|[^\w.])
52
+ (?:-?\d+(?:\.\d+)?|DATE|TIME|:[a-zA-Z_]\w*|"[^"]*"|'[^']*')
53
+ \s*\.{2,3}\s*
54
+ (?:-?\d+(?:\.\d+)?|DATE|TIME|:[a-zA-Z_]\w*|"[^"]*"|'[^']*')
55
+ (?=$|[^\w.])
56
+ /x.freeze
57
+
58
+ def self.normalize(name)
59
+ return name unless name.is_a?(String)
60
+ return name if name.empty?
61
+
62
+ normalized = name.gsub(DATE_INSPECT_PATTERN, "DATE")
63
+ normalized.gsub!(TIME_INSPECT_PATTERN, "TIME")
64
+ normalized.gsub!(CLASS_INSPECT_PATTERN) do
65
+ class_name = Regexp.last_match(1).to_s
66
+ class_name.start_with?("0x") ? "CLASS" : "CLASS:#{class_name}"
67
+ end
68
+ normalized.gsub!(NAMED_OBJECT_INSPECT_PATTERN) { "OBJECT:#{Regexp.last_match(1)}" }
69
+ normalized.gsub!(OBJECT_INSPECT_PATTERN, "OBJECT")
70
+ normalized.gsub!(ACTIVE_RECORD_CLASS_INSPECT_PATTERN) { Regexp.last_match(1).to_s }
71
+ normalized.gsub!(ARRAY_PATTERN) { |value| array_literal?(value) ? "ARRAY" : value }
72
+ normalized.gsub!(HASH_PATTERN) { |value| hash_literal?(value) ? "HASH" : value }
73
+ normalized.gsub!(TIME_PATTERN, "TIME")
74
+ normalized.gsub!(DATE_PATTERN, "DATE")
75
+ normalized.gsub!(RANGE_PATTERN) { "#{Regexp.last_match(1)}RANGE" }
76
+
77
+ normalized
78
+ rescue => e
79
+ warn_normalization_error(e)
80
+ name
81
+ end
82
+
83
+ ARRAY_PATTERN = /\[[^\[\]\n]{0,300}\]/.freeze
84
+ HASH_PATTERN = /\{[^{}\n]{0,300}\}/.freeze
85
+
86
+ def self.array_literal?(value)
87
+ content = value[1, value.length - 2].to_s.strip
88
+ return true if content.empty?
89
+
90
+ content.include?(",") || content.match?(/\A(?:"|'|:|-?\d|true\b|false\b|nil\b|#<|\[|\{)/)
91
+ end
92
+ private_class_method :array_literal?
93
+
94
+ def self.hash_literal?(value)
95
+ content = value[1, value.length - 2].to_s.strip
96
+ return true if content.empty?
97
+
98
+ content.include?("=>") ||
99
+ content.match?(/(?:\A|,)\s*(?:"[^"]+"|'[^']+'|:[a-zA-Z_]\w*|[a-zA-Z_]\w*)\s*:/)
100
+ end
101
+ private_class_method :hash_literal?
102
+
103
+ def self.warn_normalization_error(error)
104
+ Datadog.logger.warn { "Unable to normalize test name: #{error.class}: #{error.message}" }
105
+
106
+ nil
107
+ end
108
+ private_class_method :warn_normalization_error
109
+ end
110
+ end
111
+ end
112
+ end
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 34
7
+ MINOR = 35
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.34.0
4
+ version: 1.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
@@ -151,6 +151,7 @@ files:
151
151
  - lib/datadog/ci/contrib/parallel_tests/integration.rb
152
152
  - lib/datadog/ci/contrib/parallel_tests/patcher.rb
153
153
  - lib/datadog/ci/contrib/patcher.rb
154
+ - lib/datadog/ci/contrib/rspec/anonymous_example_name.rb
154
155
  - lib/datadog/ci/contrib/rspec/configuration/settings.rb
155
156
  - lib/datadog/ci/contrib/rspec/documentation_formatter.rb
156
157
  - lib/datadog/ci/contrib/rspec/example.rb
@@ -322,6 +323,7 @@ files:
322
323
  - lib/datadog/ci/utils/runtime_tags_overrides.rb
323
324
  - lib/datadog/ci/utils/stateful.rb
324
325
  - lib/datadog/ci/utils/telemetry.rb
326
+ - lib/datadog/ci/utils/test_name.rb
325
327
  - lib/datadog/ci/utils/test_run.rb
326
328
  - lib/datadog/ci/version.rb
327
329
  - lib/datadog/ci/worker.rb