gitlab_quality-test_tooling 2.20.3 → 2.21.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/Gemfile.lock +1 -1
- data/lib/gitlab_quality/test_tooling/click_house/client.rb +43 -17
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +3 -1
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +28 -1
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +18 -34
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +23 -4
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +94 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +2 -2
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5d7cbba0261ebfecefff8f84b388d3d238c4126ee183897ab63349f4995888a
|
4
|
+
data.tar.gz: 3210de6e92566a315e3bf1b07a7cd8c37e883c073a021f51cdcc5582f357e0d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4b180f20e75823e25737977cc545fd04adb46f91dd5029723d29d663b51876ac0be5f4d867bdbef20870d020d5d6ef36e7375d6c67fbfab10807c1ded2b9f42
|
7
|
+
data.tar.gz: c9f09041698d0a08032eb20b5ad26133e381d52315108f86c838b7b32bfb62d3db08a276037d7224addfb44f1779146f1ce1192fe51083075d1ca56f1104e6b5
|
data/Gemfile.lock
CHANGED
@@ -3,13 +3,12 @@
|
|
3
3
|
require "httparty"
|
4
4
|
require "json"
|
5
5
|
require "logger"
|
6
|
+
require "active_support/core_ext/object/blank"
|
6
7
|
|
7
8
|
module GitlabQuality
|
8
9
|
module TestTooling
|
9
10
|
module ClickHouse
|
10
11
|
class Client
|
11
|
-
include HTTParty
|
12
|
-
|
13
12
|
DEFAULT_BATCH_SIZE = 100_000
|
14
13
|
LOG_PREFIX = "[ClickHouse]"
|
15
14
|
|
@@ -19,14 +18,29 @@ module GitlabQuality
|
|
19
18
|
@username = username
|
20
19
|
@password = password
|
21
20
|
@logger = logger
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
# Perform sql query
|
24
|
+
#
|
25
|
+
# @param sql [String]
|
26
|
+
# @param format [String]
|
27
|
+
# @return [Array, Hash, String]
|
28
|
+
def query(sql, format: "JSONEachRow")
|
29
|
+
logger.debug("Running #{sql}")
|
30
|
+
response = post(
|
31
|
+
body: sql,
|
32
|
+
content_type: "text/plain",
|
33
|
+
query_opts: { default_format: format }
|
34
|
+
)
|
35
|
+
raise "ClickHouse query failed: code: #{response.code}, error: #{response.body}" if response.code != 200
|
28
36
|
|
29
|
-
|
37
|
+
if format == "JSONEachRow"
|
38
|
+
response.body.split("\n").map { |row| JSON.parse(row) }
|
39
|
+
elsif %w[JSON JSONCompact].include?(format)
|
40
|
+
JSON.parse(response.body.presence || "{}")
|
41
|
+
else
|
42
|
+
response.body
|
43
|
+
end
|
30
44
|
end
|
31
45
|
|
32
46
|
# Push data to ClickHouse
|
@@ -50,14 +64,14 @@ module GitlabQuality
|
|
50
64
|
|
51
65
|
err = results
|
52
66
|
.reject { |res| res[:success] }
|
53
|
-
.map { |res| "
|
67
|
+
.map { |res| "batch_size: #{res[:count]}, err: #{res[:error]}" }
|
54
68
|
.join("\n")
|
55
69
|
raise "Failures detected when pushing data to ClickHouse, errors:\n#{err}"
|
56
70
|
end
|
57
71
|
|
58
72
|
private
|
59
73
|
|
60
|
-
attr_reader :logger
|
74
|
+
attr_reader :url, :database, :username, :password, :logger
|
61
75
|
|
62
76
|
# Push batch of data
|
63
77
|
#
|
@@ -65,20 +79,32 @@ module GitlabQuality
|
|
65
79
|
# @param batch [Array<Hash>] data batch
|
66
80
|
# @return [Hash]
|
67
81
|
def send_batch(table_name, batch)
|
68
|
-
response =
|
82
|
+
response = post(
|
69
83
|
body: batch.map(&:to_json).join("\n"),
|
70
|
-
|
71
|
-
query: {
|
72
|
-
|
73
|
-
query: "INSERT INTO #{table_name} FORMAT JSONEachRow"
|
74
|
-
}
|
75
|
-
})
|
84
|
+
content_type: 'application/json',
|
85
|
+
query_opts: { query: "INSERT INTO #{table_name} FORMAT JSONEachRow" }
|
86
|
+
)
|
76
87
|
return { success: true, count: batch.size, response: response.body } if response.code == 200
|
77
88
|
|
78
89
|
{ success: false, count: batch.size, error: response.body }
|
79
90
|
rescue StandardError => e
|
80
91
|
{ success: false, count: batch.size, error: e.message }
|
81
92
|
end
|
93
|
+
|
94
|
+
# Execute post request
|
95
|
+
#
|
96
|
+
# @param body [String]
|
97
|
+
# @param content_type [String]
|
98
|
+
# @param query_opts [Hash] additional query options
|
99
|
+
# @return [HTTParty::Response]
|
100
|
+
def post(body:, content_type:, query_opts: {})
|
101
|
+
HTTParty.post(url, {
|
102
|
+
body: body,
|
103
|
+
headers: { "Content-Type" => content_type },
|
104
|
+
query: { database: database, **query_opts }.compact,
|
105
|
+
basic_auth: !!(username && password) ? { username: username, password: password } : nil
|
106
|
+
}.compact)
|
107
|
+
end
|
82
108
|
end
|
83
109
|
end
|
84
110
|
end
|
@@ -36,13 +36,15 @@ module GitlabQuality
|
|
36
36
|
# there before being released to the public repository
|
37
37
|
DIFF_PROJECT_MAPPINGS = {
|
38
38
|
'gitlab-org/quality/e2e-test-issues' => 'gitlab-org/security/gitlab',
|
39
|
+
'gitlab-org/quality/test-failure-issues' => 'gitlab-org/security/gitlab',
|
39
40
|
'gitlab-org/gitlab' => 'gitlab-org/security/gitlab',
|
40
41
|
'gitlab-org/customers-gitlab-com' => 'gitlab-org/customers-gitlab-com'
|
41
42
|
}.freeze
|
42
43
|
|
43
44
|
# Don't use the E2E test issues project for commit parent
|
44
45
|
COMMIT_PROJECT_MAPPINGS = {
|
45
|
-
'gitlab-org/quality/e2e-test-issues' => 'gitlab-org/gitlab'
|
46
|
+
'gitlab-org/quality/e2e-test-issues' => 'gitlab-org/gitlab',
|
47
|
+
'gitlab-org/quality/test-failure-issues' => 'gitlab-org/gitlab'
|
46
48
|
}.freeze
|
47
49
|
|
48
50
|
# The project contains record of the deployments we use to determine the commit diff
|
@@ -46,7 +46,7 @@ module GitlabQuality
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
attr_reader :gcs_config, :clickhouse_config
|
49
|
+
attr_reader :gcs_config, :clickhouse_config, :initial_run
|
50
50
|
attr_accessor :run_type
|
51
51
|
attr_writer :extra_rspec_metadata_keys,
|
52
52
|
:skip_record_proc,
|
@@ -72,8 +72,35 @@ module GitlabQuality
|
|
72
72
|
@clickhouse_config = config
|
73
73
|
end
|
74
74
|
|
75
|
+
# Additional columns to be created in the table if initial_run setup is used
|
76
|
+
# Columns should be defined in the format used for ALTER TABLE query, example;
|
77
|
+
# [
|
78
|
+
# "feature_category LowCardinality(String) DEFAULT ''",
|
79
|
+
# "level LowCardinality(String) DEFAULT ''"
|
80
|
+
# ]
|
81
|
+
#
|
82
|
+
# @param columns [Array]
|
83
|
+
# @return [Array]
|
84
|
+
def extra_metadata_columns=(columns)
|
85
|
+
@extra_metadata_columns = columns
|
86
|
+
end
|
87
|
+
|
75
88
|
# rubocop:enable Style/TrivialAccessors
|
76
89
|
|
90
|
+
# Marks execution as initial run and performs setup tasks before running tests, like creating database in ClickHouse
|
91
|
+
#
|
92
|
+
# @return [Boolean]
|
93
|
+
def initial_run!
|
94
|
+
@initial_run = true
|
95
|
+
end
|
96
|
+
|
97
|
+
# Additional metadata columns used during initial table creation
|
98
|
+
#
|
99
|
+
# @return [Array]
|
100
|
+
def extra_metadata_columns
|
101
|
+
@extra_metadata_columns ||= []
|
102
|
+
end
|
103
|
+
|
77
104
|
# Extra rspec metadata keys to include in exported metrics
|
78
105
|
#
|
79
106
|
# @return [Array<Symbol>]
|
@@ -2,16 +2,25 @@
|
|
2
2
|
|
3
3
|
require "rspec/core/formatters/base_formatter"
|
4
4
|
|
5
|
-
# rubocop:disable Metrics/AbcSize
|
6
5
|
module GitlabQuality
|
7
6
|
module TestTooling
|
8
7
|
module TestMetricsExporter
|
9
8
|
class Formatter < RSpec::Core::Formatters::BaseFormatter
|
10
|
-
|
9
|
+
include Utils
|
10
|
+
|
11
|
+
RSpec::Core::Formatters.register(self, *[:stop, Utils.config.initial_run ? :start : nil].compact)
|
11
12
|
|
12
13
|
LOG_PREFIX = "[MetricsExporter]"
|
13
14
|
|
15
|
+
def start(_notification)
|
16
|
+
logger.debug("#{LOG_PREFIX} Running initial setup for metrics export")
|
17
|
+
return logger.warn("#{LOG_PREFIX} Initial setup is enabled, but clickhouse configuration is missing!") unless clickhouse_config
|
18
|
+
|
19
|
+
create_clickhouse_metrics_table
|
20
|
+
end
|
21
|
+
|
14
22
|
def stop(notification)
|
23
|
+
logger.debug("#{LOG_PREFIX} Starting test metrics export")
|
15
24
|
data = notification.examples.filter_map do |example|
|
16
25
|
next if config.skip_record_proc.call(example)
|
17
26
|
|
@@ -25,19 +34,7 @@ module GitlabQuality
|
|
25
34
|
|
26
35
|
private
|
27
36
|
|
28
|
-
|
29
|
-
#
|
30
|
-
# @return [Config]
|
31
|
-
def config
|
32
|
-
Config.configuration
|
33
|
-
end
|
34
|
-
|
35
|
-
# Logger instance
|
36
|
-
#
|
37
|
-
# @return [Logger]
|
38
|
-
def logger
|
39
|
-
config.logger
|
40
|
-
end
|
37
|
+
delegate :gcs_config, :clickhouse_config, to: :config
|
41
38
|
|
42
39
|
# Single common timestamp for all exported example metrics to keep data points consistently grouped
|
43
40
|
#
|
@@ -54,14 +51,9 @@ module GitlabQuality
|
|
54
51
|
# @param data [Array]
|
55
52
|
# @return [void]
|
56
53
|
def push_to_gcs(data)
|
57
|
-
return logger.debug("#{LOG_PREFIX} GCS configuration missing, skipping gcs export!") unless
|
58
|
-
|
59
|
-
gcs_config
|
60
|
-
GcsTools.gcs_client(
|
61
|
-
project_id: gcs_config.project_id,
|
62
|
-
credentials: gcs_config.credentials,
|
63
|
-
dry_run: gcs_config.dry_run
|
64
|
-
).put_object(gcs_config.bucket_name, gcs_config.metrics_file_name, data.to_json)
|
54
|
+
return logger.debug("#{LOG_PREFIX} GCS configuration missing, skipping gcs export!") unless gcs_config
|
55
|
+
|
56
|
+
gcs_client.put_object(gcs_config.bucket_name, gcs_config.metrics_file_name, data.to_json)
|
65
57
|
logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to GCS bucket!")
|
66
58
|
rescue StandardError => e
|
67
59
|
logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to GCS: #{e.message}")
|
@@ -72,16 +64,9 @@ module GitlabQuality
|
|
72
64
|
# @param data [Array<Hash>]
|
73
65
|
# @return [void]
|
74
66
|
def push_to_clickhouse(data)
|
75
|
-
return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless
|
76
|
-
|
77
|
-
clickhouse_config
|
78
|
-
ClickHouse::Client.new(
|
79
|
-
url: clickhouse_config.url,
|
80
|
-
database: clickhouse_config.database,
|
81
|
-
username: clickhouse_config.username,
|
82
|
-
password: clickhouse_config.password,
|
83
|
-
logger: logger
|
84
|
-
).insert_json_data(clickhouse_config.table_name, data)
|
67
|
+
return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless clickhouse_config
|
68
|
+
|
69
|
+
clickhouse_client.insert_json_data(clickhouse_config.table_name, data)
|
85
70
|
logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to ClickHouse!")
|
86
71
|
rescue StandardError => e
|
87
72
|
logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to ClickHouse: #{e.message}")
|
@@ -90,4 +75,3 @@ module GitlabQuality
|
|
90
75
|
end
|
91
76
|
end
|
92
77
|
end
|
93
|
-
# rubocop:enable Metrics/AbcSize
|
@@ -39,15 +39,17 @@ module GitlabQuality
|
|
39
39
|
# @return [Hash]
|
40
40
|
def rspec_metrics # rubocop:disable Metrics/AbcSize
|
41
41
|
{
|
42
|
-
id: example.id,
|
42
|
+
id: without_relative_path(example.id),
|
43
43
|
name: example.full_description,
|
44
|
-
|
44
|
+
hash: OpenSSL::Digest.hexdigest("SHA256", "#{file_path}#{example.full_description}")[..40],
|
45
|
+
file_path: file_path,
|
45
46
|
status: example.execution_result.status,
|
46
47
|
run_time: (example.execution_result.run_time * 1000).round,
|
47
48
|
location: example_location,
|
48
49
|
exception_class: exception_class,
|
49
50
|
failure_exception: failure_exception,
|
50
51
|
quarantined: quarantined?,
|
52
|
+
feature_category: example.metadata[:feature_category] || "",
|
51
53
|
test_retried: config.test_retried_proc.call(example)
|
52
54
|
}
|
53
55
|
end
|
@@ -102,6 +104,8 @@ module GitlabQuality
|
|
102
104
|
#
|
103
105
|
# @return [String]
|
104
106
|
def example_location
|
107
|
+
return @example_location if @example_location
|
108
|
+
|
105
109
|
# ensures that location will be correct even in case of shared examples
|
106
110
|
file = example
|
107
111
|
.metadata
|
@@ -109,9 +113,16 @@ module GitlabQuality
|
|
109
113
|
.last
|
110
114
|
&.formatted_inclusion_location
|
111
115
|
|
112
|
-
return example.location unless file
|
116
|
+
return without_relative_path(example.location) unless file
|
117
|
+
|
118
|
+
@example_location = without_relative_path(file)
|
119
|
+
end
|
113
120
|
|
114
|
-
|
121
|
+
# File path based on actual test location, not shared example location
|
122
|
+
#
|
123
|
+
# @return [String]
|
124
|
+
def file_path
|
125
|
+
@file_path ||= example_location.gsub(/:\d+$/, "")
|
115
126
|
end
|
116
127
|
|
117
128
|
# Failure exception class
|
@@ -166,6 +177,14 @@ module GitlabQuality
|
|
166
177
|
def bool?(val)
|
167
178
|
[true, false].include?(val)
|
168
179
|
end
|
180
|
+
|
181
|
+
# Path without leading ./
|
182
|
+
#
|
183
|
+
# @param path [String]
|
184
|
+
# @return [String]
|
185
|
+
def without_relative_path(path)
|
186
|
+
path.gsub(%r{^\./}, "")
|
187
|
+
end
|
169
188
|
end
|
170
189
|
end
|
171
190
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module TestMetricsExporter
|
6
|
+
module Utils
|
7
|
+
# Instance of metrics exporter configuration
|
8
|
+
#
|
9
|
+
# @return [Config]
|
10
|
+
def config
|
11
|
+
Config.configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
# Configured logger instance
|
15
|
+
#
|
16
|
+
# @return [Logger]
|
17
|
+
def logger
|
18
|
+
config.logger
|
19
|
+
end
|
20
|
+
|
21
|
+
# Configured clickhouse client
|
22
|
+
#
|
23
|
+
# @return [ClickHouse::Client]
|
24
|
+
def clickhouse_client
|
25
|
+
ClickHouse::Client.new(
|
26
|
+
url: config.clickhouse_config.url,
|
27
|
+
database: config.clickhouse_config.database,
|
28
|
+
username: config.clickhouse_config.username,
|
29
|
+
password: config.clickhouse_config.password,
|
30
|
+
logger: logger
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# GCS client instance
|
35
|
+
#
|
36
|
+
# @return [Fog::Storage::Google, GcsTools::GCSMockClient]
|
37
|
+
def gcs_client
|
38
|
+
GcsTools.gcs_client(
|
39
|
+
project_id: config.gcs_config.project_id,
|
40
|
+
credentials: config.gcs_config.credentials,
|
41
|
+
dry_run: config.gcs_config.dry_run
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create table for metrics export using current ClickHouse configuration
|
46
|
+
#
|
47
|
+
# This method is mostly for schema documentation but it can be used together with initial_run! method in
|
48
|
+
#
|
49
|
+
# @return [void]
|
50
|
+
def create_clickhouse_metrics_table
|
51
|
+
table_name = config.clickhouse_config.table_name
|
52
|
+
|
53
|
+
clickhouse_client.query(<<~SQL)
|
54
|
+
CREATE TABLE IF NOT EXISTS #{table_name}
|
55
|
+
(
|
56
|
+
timestamp DateTime64(6, 'UTC'),
|
57
|
+
id String,
|
58
|
+
name String,
|
59
|
+
hash String,
|
60
|
+
file_path String,
|
61
|
+
status LowCardinality(String),
|
62
|
+
run_time UInt32,
|
63
|
+
location String,
|
64
|
+
quarantined Bool,
|
65
|
+
test_retried Bool,
|
66
|
+
feature_category LowCardinality(String) DEFAULT '',
|
67
|
+
ci_project_id UInt32,
|
68
|
+
ci_job_name LowCardinality(String),
|
69
|
+
ci_job_id UInt64,
|
70
|
+
ci_pipeline_id UInt64,
|
71
|
+
ci_merge_request_iid UInt32 DEFAULT 0,
|
72
|
+
ci_project_path LowCardinality(String),
|
73
|
+
ci_branch String,
|
74
|
+
ci_target_branch LowCardinality(String),
|
75
|
+
exception_class String DEFAULT '',
|
76
|
+
failure_exception String DEFAULT ''
|
77
|
+
)
|
78
|
+
ENGINE = MergeTree()
|
79
|
+
PARTITION BY toYYYYMM(timestamp)
|
80
|
+
ORDER BY (ci_project_path, timestamp, status, feature_category, file_path, ci_pipeline_id)
|
81
|
+
SETTINGS index_granularity = 8192;
|
82
|
+
SQL
|
83
|
+
return if config.extra_metadata_columns.empty?
|
84
|
+
|
85
|
+
clickhouse_client.query(
|
86
|
+
"ALTER TABLE #{table_name} #{config.extra_metadata_columns.map { |column| "ADD COLUMN IF NOT EXISTS #{column}" }.join(', ')};"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
module_function :config, :logger, :clickhouse_client
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -119,7 +119,7 @@ module GitlabQuality
|
|
119
119
|
end
|
120
120
|
|
121
121
|
def file_base_url
|
122
|
-
@file_base_url ||= "https://gitlab.com/#{
|
122
|
+
@file_base_url ||= "https://gitlab.com/#{mapped_project}/-/blob/#{ref}/"
|
123
123
|
end
|
124
124
|
|
125
125
|
def test_file_link
|
@@ -158,7 +158,7 @@ module GitlabQuality
|
|
158
158
|
attr_reader :token, :project, :ref
|
159
159
|
|
160
160
|
def mapped_project
|
161
|
-
if
|
161
|
+
if ['gitlab-org/quality/e2e-test-issues', 'gitlab-org/quality/test-failure-issues'].include?(project)
|
162
162
|
'gitlab-org/gitlab'
|
163
163
|
else
|
164
164
|
project
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab_quality-test_tooling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.21.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab Quality
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-10-
|
11
|
+
date: 2025-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -580,6 +580,7 @@ files:
|
|
580
580
|
- lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb
|
581
581
|
- lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb
|
582
582
|
- lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb
|
583
|
+
- lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb
|
583
584
|
- lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
|
584
585
|
- lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb
|
585
586
|
- lib/gitlab_quality/test_tooling/test_result/json_test_result.rb
|