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 +4 -4
- data/CHANGELOG.md +17 -2
- data/lib/datadog/ci/code_coverage/component.rb +26 -2
- data/lib/datadog/ci/configuration/components.rb +2 -1
- data/lib/datadog/ci/configuration/settings.rb +5 -0
- data/lib/datadog/ci/contrib/minitest/helpers.rb +21 -6
- 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/simplecov/result_extractor.rb +9 -1
- data/lib/datadog/ci/ext/environment/extractor.rb +1 -0
- data/lib/datadog/ci/ext/environment/providers/base.rb +3 -0
- data/lib/datadog/ci/ext/environment/providers/buildkite.rb +4 -0
- data/lib/datadog/ci/ext/environment.rb +1 -0
- data/lib/datadog/ci/ext/settings.rb +1 -0
- data/lib/datadog/ci/ext/telemetry.rb +3 -0
- data/lib/datadog/ci/test.rb +67 -0
- data/lib/datadog/ci/test_discovery/component.rb +4 -0
- data/lib/datadog/ci/test_impact_analysis/component.rb +36 -8
- data/lib/datadog/ci/test_impact_analysis/coverage/event.rb +24 -15
- data/lib/datadog/ci/test_impact_analysis/coverage/files.rb +66 -0
- data/lib/datadog/ci/test_suite.rb +67 -0
- data/lib/datadog/ci/test_tracing/component.rb +8 -0
- data/lib/datadog/ci/test_tracing/deprecated_total_coverage_metric.rb +11 -1
- data/lib/datadog/ci/test_tracing/known_tests.rb +20 -0
- data/lib/datadog/ci/utils/test_name.rb +112 -0
- data/lib/datadog/ci/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 35241d1cd03e2ca4c3eca7c92979991abf620452a57b25bb1452f799a973706b
|
|
4
|
+
data.tar.gz: 953eba8e467af1a595ba9f75a5d017dbc147002e8cd938d07df5162efffb3e24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
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
|