datadog-ci 1.34.0 → 1.36.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: 35241d1cd03e2ca4c3eca7c92979991abf620452a57b25bb1452f799a973706b
4
+ data.tar.gz: 953eba8e467af1a595ba9f75a5d017dbc147002e8cd938d07df5162efffb3e24
5
5
  SHA512:
6
- metadata.gz: 75d99396decbd20bb93cc2e061295be1e55c033b41980216bc0bb9229fd36269e7ad8bd5ad1104203ca10a32bed668aa65f41d01ddd5c8ebef34d206397a7c25
7
- data.tar.gz: 3a0efeaaad0042cd836c51a7232a0348b377acf959bc6e10670e01ed5d7ea863caf60853142cb7e2d563da1c64ef1dadee5c3735a69bf482dfdb7b611239767d
6
+ metadata.gz: 0eb4f023eccfd837e7bcd3f4c67ff3f727e1afb41c1f4f7078a81bb3a856b42c6c35fb003e94227911755f06698d829a48aefd97cf75edca968ac15806e36e61
7
+ data.tar.gz: 169fa6ad3fe020aed1293fbc80e893715c0f0ff23b0fc089b382876b2b29fe37a0e0d8b205a945e4e0d7184a01cb559a2ea814fdf47baa4dbc4024b7fc5124b6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.36.0] - 2026-08-03
4
+
5
+ ### Added
6
+
7
+ * Support custom impacted files in Test Impact Analysis ([#554][])
8
+ * Add ci.pipeline.display_name tag for Buildkite ([#555][])
9
+ * Add code coverage report flags ([#542][])
10
+
11
+ ## [1.35.0] - 2026-07-23
12
+
3
13
  ## [1.34.0] - 2026-06-25
4
14
 
5
15
  ### Added
@@ -665,7 +675,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
665
675
 
666
676
  - Ruby versions < 2.7 no longer supported ([#8][])
667
677
 
668
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...main
678
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.36.0...main
679
+ [1.36.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.35.0...v1.36.0
680
+ [1.35.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...v1.35.0
669
681
  [1.34.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.33.0...v1.34.0
670
682
  [1.33.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.32.0...v1.33.0
671
683
  [1.32.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.31.0...v1.32.0
@@ -941,4 +953,7 @@ Currently test suite level visibility is not used by our instrumentation: it wil
941
953
  [#521]: https://github.com/DataDog/datadog-ci-rb/issues/521
942
954
  [#522]: https://github.com/DataDog/datadog-ci-rb/issues/522
943
955
  [#527]: https://github.com/DataDog/datadog-ci-rb/issues/527
944
- [#530]: https://github.com/DataDog/datadog-ci-rb/issues/530
956
+ [#530]: https://github.com/DataDog/datadog-ci-rb/issues/530
957
+ [#542]: https://github.com/DataDog/datadog-ci-rb/issues/542
958
+ [#554]: https://github.com/DataDog/datadog-ci-rb/issues/554
959
+ [#555]: https://github.com/DataDog/datadog-ci-rb/issues/555
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../ext/environment"
4
+ require_relative "../ext/settings"
4
5
  require_relative "transport"
5
6
 
6
7
  module Datadog
@@ -10,12 +11,14 @@ module Datadog
10
11
  # to Datadog's Code Coverage product.
11
12
  class Component
12
13
  COVERAGE_REPORT_TYPE = "coverage_report"
14
+ MAX_REPORT_FLAGS = 32
13
15
 
14
16
  attr_reader :enabled
15
17
 
16
- def initialize(enabled:, transport:)
18
+ def initialize(enabled:, transport:, flags:)
17
19
  @enabled = enabled
18
20
  @transport = transport
21
+ @flags = parse_flags(flags)
19
22
  end
20
23
 
21
24
  def configure(library_configuration)
@@ -44,10 +47,31 @@ module Datadog
44
47
  private
45
48
 
46
49
  def build_event(format)
47
- {
50
+ event = {
48
51
  "type" => COVERAGE_REPORT_TYPE,
49
52
  "format" => format
50
53
  }.merge(Ext::Environment.tags(ENV))
54
+
55
+ return event unless @flags
56
+
57
+ event.merge("report.flags" => @flags)
58
+ end
59
+
60
+ def parse_flags(value)
61
+ return if value.nil?
62
+
63
+ flags = value.split(",").map(&:strip).reject(&:empty?)
64
+ return if flags.empty?
65
+
66
+ if flags.length > MAX_REPORT_FLAGS
67
+ Datadog.logger.warn(
68
+ "#{Ext::Settings::ENV_CODE_COVERAGE_FLAGS} contains #{flags.length} flags, " \
69
+ "but only #{MAX_REPORT_FLAGS} are allowed; omitting report flags"
70
+ )
71
+ return
72
+ end
73
+
74
+ flags.freeze
51
75
  end
52
76
  end
53
77
  end
@@ -312,7 +312,8 @@ module Datadog
312
312
 
313
313
  CodeCoverage::Component.new(
314
314
  enabled: settings.ci.code_coverage_report_upload_enabled,
315
- transport: CodeCoverage::Transport.new(api: api)
315
+ transport: CodeCoverage::Transport.new(api: api),
316
+ flags: settings.ci.code_coverage_flags
316
317
  )
317
318
  end
318
319
 
@@ -224,6 +224,11 @@ module Datadog
224
224
  o.default true
225
225
  end
226
226
 
227
+ option :code_coverage_flags do |o|
228
+ o.type :string, nilable: true
229
+ o.env CI::Ext::Settings::ENV_CODE_COVERAGE_FLAGS
230
+ end
231
+
227
232
  option :runtime_tags_overrides do |o|
228
233
  o.type :string, nilable: true
229
234
  o.env CI::Ext::Settings::ENV_RUNTIME_TAGS
@@ -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