gitlab_quality-test_tooling 3.16.0 → 3.19.1

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.
@@ -1,228 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'time'
4
-
5
- module GitlabQuality
6
- module TestTooling
7
- module TestMetricsExporter
8
- class TestMetrics
9
- def initialize(example, timestamp)
10
- @example = example
11
- @timestamp = timestamp
12
- end
13
-
14
- # Test data hash
15
- #
16
- # @return [Hash]
17
- def data
18
- {
19
- timestamp: timestamp,
20
- **rspec_metrics,
21
- **ci_metrics,
22
- **custom_metrics
23
- }.compact
24
- end
25
-
26
- private
27
-
28
- attr_reader :example, :timestamp
29
-
30
- # Exporter configuration
31
- #
32
- # @return [Config]
33
- def config
34
- Config.configuration
35
- end
36
-
37
- # Rspec related metrics
38
- #
39
- # @return [Hash]
40
- def rspec_metrics # rubocop:disable Metrics/AbcSize
41
- {
42
- id: without_relative_path(example.id),
43
- name: example.full_description,
44
- hash: OpenSSL::Digest.hexdigest("SHA256", "#{file_path}#{example.full_description}")[..40],
45
- file_path: file_path,
46
- status: example.execution_result.status,
47
- run_time: (example.execution_result.run_time * 1000).round,
48
- location: example_location,
49
- # TODO: remove exception_class once migration to exception_classes is fully complete on clickhouse side
50
- exception_class: example.execution_result.exception&.class&.to_s,
51
- exception_classes: exception_classes.map { |e| e.class.to_s }.uniq,
52
- failure_exception: failure_exception,
53
- quarantined: quarantined?,
54
- quarantine_issue_url: quarantine_issue_url || "",
55
- feature_category: example.metadata[:feature_category] || "",
56
- test_retried: config.test_retried_proc.call(example),
57
- run_type: run_type,
58
- spec_file_path_prefix: config.spec_file_path_prefix
59
- }
60
- end
61
-
62
- # CI related metrics
63
- #
64
- # @return [Hash]
65
- def ci_metrics
66
- {
67
- ci_project_id: env("CI_PROJECT_ID")&.to_i,
68
- ci_project_path: env("CI_PROJECT_PATH"),
69
- ci_job_name: ci_job_name,
70
- ci_job_id: env('CI_JOB_ID')&.to_i,
71
- ci_pipeline_id: env('CI_PIPELINE_ID')&.to_i,
72
- ci_merge_request_iid: (env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID'))&.to_i,
73
- ci_branch: env("CI_COMMIT_REF_NAME"),
74
- ci_target_branch: env("CI_MERGE_REQUEST_TARGET_BRANCH_NAME"),
75
- ci_server_url: env("CI_SERVER_URL")
76
- }
77
- end
78
-
79
- # Additional custom metrics
80
- #
81
- # @return [Hash]
82
- def custom_metrics
83
- metrics = example.metadata
84
- .slice(*config.extra_rspec_metadata_keys)
85
- .merge(config.custom_metrics_proc.call(example))
86
-
87
- metrics.each_with_object({}) do |(k, value), custom_metrics|
88
- custom_metrics[k.to_sym] = metrics_value(value)
89
- end
90
- end
91
-
92
- # Checks if spec is quarantined
93
- #
94
- # @return [String]
95
- def quarantined?
96
- return false unless example.metadata.key?(:quarantine)
97
-
98
- # if quarantine key is present and status is pending, consider it quarantined
99
- example.execution_result.status == :pending
100
- end
101
-
102
- # Extract quarantine issue URL from metadata
103
- #
104
- # @return [String, nil]
105
- def quarantine_issue_url
106
- return nil unless example.metadata.key?(:quarantine)
107
-
108
- metadata = example.metadata[:quarantine]
109
- case metadata
110
- when String
111
- # Direct URL: quarantine: 'https://gitlab.com/.../issues/123'
112
- metadata if metadata.start_with?('http')
113
- when Hash
114
- # Hash format: quarantine: { issue: 'https://...', reason: '...' }
115
- issue = metadata[:issue] || metadata['issue']
116
- # Handle array of URLs (take the first one)
117
- issue.is_a?(Array) ? issue.first : issue
118
- end
119
- end
120
-
121
- # Base ci job name
122
- #
123
- # @return [String]
124
- def ci_job_name
125
- env("CI_JOB_NAME")&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
126
- end
127
-
128
- # Example location
129
- #
130
- # @return [String]
131
- def example_location
132
- return @example_location if @example_location
133
-
134
- # ensures that location will be correct even in case of shared examples
135
- file = example
136
- .metadata
137
- .fetch(:shared_group_inclusion_backtrace)
138
- .last
139
- &.formatted_inclusion_location
140
-
141
- return without_relative_path(example.location) unless file
142
-
143
- @example_location = without_relative_path(file)
144
- end
145
-
146
- # File path based on actual test location, not shared example location
147
- #
148
- # @return [String]
149
- def file_path
150
- @file_path ||= example_location.gsub(/:\d+$/, "")
151
- end
152
-
153
- # Failure exception classes
154
- #
155
- # @return [Array<Exception>]
156
- def exception_classes
157
- exception = example.execution_result.exception
158
- return [] unless exception
159
- return [exception] unless exception.respond_to?(:all_exceptions)
160
-
161
- exception.all_exceptions.flatten
162
- end
163
-
164
- # Truncated exception message
165
- #
166
- # For MultipleExceptionError, returns the first wrapped exception's message
167
- # instead of the unhelpful wrapper class name.
168
- #
169
- # @return [String]
170
- def failure_exception
171
- exception = example.execution_result.exception
172
- return unless exception
173
-
174
- source = if exception.respond_to?(:all_exceptions)
175
- exception.all_exceptions.flatten.first || exception
176
- else
177
- exception
178
- end
179
-
180
- source.to_s.tr("\n", " ").slice(0, 1000)
181
- end
182
-
183
- # Test run type | suite name
184
- #
185
- # @return [String]
186
- def run_type
187
- config.run_type || ci_job_name || "unknown"
188
- end
189
-
190
- # Return non empty environment variable value
191
- #
192
- # @param [String] name
193
- # @return [String, nil]
194
- def env(name)
195
- return unless ENV[name] && !ENV[name].empty?
196
-
197
- ENV.fetch(name)
198
- end
199
-
200
- # Metrics value cast to a valid type
201
- #
202
- # @param value [Object]
203
- # @return [Object]
204
- def metrics_value(value)
205
- return value if value.is_a?(Numeric) || value.is_a?(String) || bool?(value) || value.nil?
206
-
207
- value.to_s
208
- end
209
-
210
- # Value is a true or false
211
- #
212
- # @param val [Object]
213
- # @return [Boolean]
214
- def bool?(val)
215
- [true, false].include?(val)
216
- end
217
-
218
- # Path without leading ./
219
- #
220
- # @param path [String]
221
- # @return [String]
222
- def without_relative_path(path)
223
- path.gsub(%r{^\./}, "")
224
- end
225
- end
226
- end
227
- end
228
- end