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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fcdafab955309e5bc8339d69af869089359e4323f4b51ba80565092443928df
4
- data.tar.gz: fe09638d129c26153885ea49ebed73834fbd87e7bc65f4cd79ff3807027d47c3
3
+ metadata.gz: a5d7cbba0261ebfecefff8f84b388d3d238c4126ee183897ab63349f4995888a
4
+ data.tar.gz: 3210de6e92566a315e3bf1b07a7cd8c37e883c073a021f51cdcc5582f357e0d6
5
5
  SHA512:
6
- metadata.gz: aa9dab59ef59c0c3fedce10911290974a8e0fe65746da8e38c0ac33697808e936b8386b7252fa6a55ccc11df2fadadfa46467b390749bf41db9a273f03e698a8
7
- data.tar.gz: f03234f295299537ae028678e8039754f8e15fe988599ef688ef2bd3feb9e8b52fad07f6dde9a892057d4ea9da096e029612feb549bc929259bea573f322c33d
6
+ metadata.gz: f4b180f20e75823e25737977cc545fd04adb46f91dd5029723d29d663b51876ac0be5f4d867bdbef20870d020d5d6ef36e7375d6c67fbfab10807c1ded2b9f42
7
+ data.tar.gz: c9f09041698d0a08032eb20b5ad26133e381d52315108f86c838b7b32bfb62d3db08a276037d7224addfb44f1779146f1ce1192fe51083075d1ca56f1104e6b5
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (2.20.3)
4
+ gitlab_quality-test_tooling (2.21.0)
5
5
  activesupport (>= 7.0, < 7.3)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
@@ -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
- # Set base URI
24
- self.class.base_uri @url
25
-
26
- # Set basic auth if credentials provided
27
- return unless @username && @password
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
- self.class.basic_auth @username, @password
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| "size: #{res[:count]}, err: #{res[:error]}" }
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 = self.class.post('/', {
82
+ response = post(
69
83
  body: batch.map(&:to_json).join("\n"),
70
- headers: { 'Content-Type' => 'application/json' },
71
- query: {
72
- database: @database,
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
- RSpec::Core::Formatters.register(self, :stop)
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
- # Configuration instance
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 config.gcs_config
58
-
59
- gcs_config = config.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 config.clickhouse_config
76
-
77
- clickhouse_config = config.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
- file_path: example.metadata[:file_path].sub(/\A./, ''),
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
- file
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/#{project == 'gitlab-org/quality/e2e-test-issues' ? 'gitlab-org/gitlab' : project}/-/blob/#{ref}/"
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 project == 'gitlab-org/quality/e2e-test-issues'
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "2.20.3"
5
+ VERSION = "2.21.0"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rainbow/refinement'
4
4
  require 'zeitwerk'
5
+ require 'active_support/core_ext/module/delegation'
5
6
 
6
7
  module GitlabQuality
7
8
  module TestTooling
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.20.3
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-08 00:00:00.000000000 Z
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