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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -2
- data/lib/datadog/ci/configuration/components.rb +2 -0
- data/lib/datadog/ci/configuration/settings.rb +11 -0
- data/lib/datadog/ci/contrib/cucumber/formatter.rb +51 -1
- data/lib/datadog/ci/contrib/cucumber/instrumentation.rb +5 -0
- data/lib/datadog/ci/contrib/minitest/helpers.rb +54 -6
- data/lib/datadog/ci/contrib/minitest/runnable.rb +5 -0
- data/lib/datadog/ci/contrib/minitest/runnable_minitest_6.rb +5 -0
- data/lib/datadog/ci/contrib/minitest/test.rb +21 -27
- data/lib/datadog/ci/contrib/patcher.rb +0 -4
- data/lib/datadog/ci/contrib/rspec/anonymous_example_name.rb +515 -0
- data/lib/datadog/ci/contrib/rspec/example.rb +23 -5
- data/lib/datadog/ci/contrib/rspec/example_group.rb +11 -1
- data/lib/datadog/ci/contrib/simplecov/result_extractor.rb +9 -1
- data/lib/datadog/ci/ext/settings.rb +1 -0
- data/lib/datadog/ci/ext/telemetry.rb +1 -0
- data/lib/datadog/ci/ext/test.rb +8 -1
- data/lib/datadog/ci/remote/library_settings_client.rb +3 -2
- data/lib/datadog/ci/source_code/method_inspect.rb +0 -3
- data/lib/datadog/ci/test_discovery/component.rb +4 -0
- data/lib/datadog/ci/test_impact_analysis/component.rb +176 -59
- data/lib/datadog/ci/test_impact_analysis/coverage/event.rb +6 -4
- data/lib/datadog/ci/test_impact_analysis/null_component.rb +25 -2
- data/lib/datadog/ci/test_impact_analysis/skippable.rb +20 -5
- data/lib/datadog/ci/test_impact_analysis/skippable_percentage/estimator.rb +3 -3
- data/lib/datadog/ci/test_optimization_cache/readers/legacy.rb +1 -1
- data/lib/datadog/ci/test_suite.rb +20 -0
- data/lib/datadog/ci/test_tracing/component.rb +10 -0
- data/lib/datadog/ci/test_tracing/deprecated_total_coverage_metric.rb +11 -1
- data/lib/datadog/ci/utils/configuration.rb +12 -0
- data/lib/datadog/ci/utils/test_name.rb +112 -0
- data/lib/datadog/ci/version.rb +1 -1
- 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
|
|
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
|
-
|
|
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"
|
data/lib/datadog/ci/ext/test.rb
CHANGED
|
@@ -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
|
-
|
|
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" =>
|
|
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,
|