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.
- checksums.yaml +4 -4
- data/AGENTS.md +7 -4
- data/Gemfile.lock +1 -1
- data/README.md +11 -0
- data/exe/relate-failure-issue +10 -0
- data/exe/test-coverage +91 -63
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/per_test_coverage_table.rb +41 -8
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql +33 -16
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshot.sql +31 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter.rb +95 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_data.rb +16 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_exporter.rb +181 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +24 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +44 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +5 -7
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/client.rb +0 -50
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +0 -88
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config_helper.rb +0 -115
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +0 -67
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +0 -228
|
@@ -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
|