gitlab_quality-test_tooling 3.11.0 → 3.13.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: c0f293cc7660b976ec011dc0e44e8f4a636b0170f99206c699bba5e59db989f7
4
- data.tar.gz: 1205e282fbdbe7faf15c421e9907840ca59662488206ccb2007dfdd96254e934
3
+ metadata.gz: e1284d833aad5c38a45cff835cf3e701193117aa9694293b42fee9ce60345b0d
4
+ data.tar.gz: e209cf22987a7a19c58a4675ad529a6910ec387117a49fbcef97aadef9230c6f
5
5
  SHA512:
6
- metadata.gz: 49052d3d0eade661c58479770a971ef2e8f5377cf13af619dd4e5cc9a8d6f5759d2b5ab7d41e7ad8aea5da490c8824da39a0c4fb6a45dd9ec6d17931f039faa3
7
- data.tar.gz: 2bf4dca1ba6d1a34e262058843ecc81d273897a931de180bbe544b956f8568de4ad4e9bd0677f753f667c4fb965639e23e3c6e7af0e0f26f04bd3ea101ac995b
6
+ metadata.gz: '09ded437ab350eddf3736017e844865ef0a5ca311c6d62e800b87b272e691eab904ae12c6241f202f76ae1afa1e717880b99984e640fbd9728791e684fbfbbed'
7
+ data.tar.gz: a9064f95ca94ef8d8d476f097f3ee9412d71dca931a715e9f446a81f8646e75e14ab141b93fe83ebd1bf898d32503b4806179ac2f9b3233fe69c95024c693491
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.11.0)
4
+ gitlab_quality-test_tooling (3.13.0)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
8
8
  gitlab (>= 4.19, < 7.0)
9
9
  http (~> 5.0)
10
+ http-cookie (>= 1.0, < 1.1.1)
10
11
  nokogiri (~> 1.10)
11
12
  parallel (>= 1, < 2)
12
13
  rainbow (>= 3, < 4)
@@ -34,6 +34,29 @@ module GitlabQuality
34
34
  categories_mapping.dig(feature_category, 'group')
35
35
  end
36
36
 
37
+ def group_data_from_label(label)
38
+ return unless label
39
+
40
+ groups_mapping.each do |key, data|
41
+ next unless label.casecmp?(data['label'])
42
+
43
+ return data.merge('key' => key)
44
+ end
45
+
46
+ nil
47
+ end
48
+
49
+ def product_managers_from_product_group(product_group)
50
+ group_data = groups_mapping[product_group]
51
+ return [] unless group_data
52
+
53
+ normalize_mentions(Array(group_data['product_managers']).compact)
54
+ end
55
+
56
+ def all_group_labels
57
+ groups_mapping.filter_map { |_key, data| data['label'] }.uniq
58
+ end
59
+
37
60
  def engineering_managers_from_product_group(product_group, *scopes)
38
61
  group_data = groups_mapping[product_group]
39
62
  return [] unless group_data
@@ -42,11 +65,15 @@ module GitlabQuality
42
65
  scopes = [:all] if default_scope
43
66
  ems = resolve_ems(group_data, scopes, default_scope)
44
67
 
45
- ems.uniq.sort.map { |name| name.start_with?('@') ? name : "@#{name}" }
68
+ normalize_mentions(ems)
46
69
  end
47
70
 
48
71
  private
49
72
 
73
+ def normalize_mentions(names)
74
+ names.uniq.sort.map { |name| name.start_with?('@') ? name : "@#{name}" }
75
+ end
76
+
50
77
  def resolve_ems(group_data, scopes, default_scope)
51
78
  keys = scopes.flat_map { |scope| EM_KEYS.fetch(scope) { [] } }
52
79
  ems = group_data.values_at(*keys).flatten.compact
@@ -7,6 +7,8 @@ module GitlabQuality
7
7
  SLOW_TEST_MESSAGE = '<!-- slow-test -->'
8
8
  SLOW_TEST_LABEL = '/label ~"rspec:slow test detected"'
9
9
  SLOW_TEST_NOTE_SOURCE_CODE = 'Generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/blob/main/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb).'
10
+ SLOW_TEST_UNHEALTHY_PATTERNS = 'https://docs.gitlab.com/development/testing_guide/unhealthy_tests/#common-patterns-that-cause-slow-tests'
11
+ SLOW_TEST_BEST_PRACTICES = 'https://docs.gitlab.com/development/testing_guide/best_practices/#test-slowness'
10
12
 
11
13
  def initialize(token:, input_files:, merge_request_iid:, project: nil, dry_run: false, **_kwargs)
12
14
  @token = token
@@ -70,23 +72,36 @@ module GitlabQuality
70
72
  SLOW_TEST_LABEL,
71
73
  ":tools: #{SLOW_TEST_NOTE_SOURCE_CODE}\n",
72
74
  "---\n",
73
- ":snail: Slow tests detected in this merge request. These slow tests might be related to this merge request's changes.",
74
- "<details><summary>Click to expand</summary>\n",
75
- '| Job | File | Name | Duration | Expected duration |',
76
- '| --- | --- | --- | --- | --- |'
75
+ ":snail: Slow tests detected in this merge request. These slow tests might be related to this merge request's changes."
77
76
  ]
78
77
  end
79
78
 
79
+ def handbook_guidance
80
+ ":books: For guidance on improving test performance, see " \
81
+ "[Common Patterns That Cause Slow Tests](#{SLOW_TEST_UNHEALTHY_PATTERNS}) " \
82
+ "and [Best Practices – Test Slowness](#{SLOW_TEST_BEST_PRACTICES})."
83
+ end
84
+
85
+ def feature_spec?(slow_test)
86
+ slow_test.file.include?('spec/features/')
87
+ end
88
+
80
89
  def slow_test_rows(slow_test)
81
90
  slow_test.map do |test|
82
91
  slow_test_table_row(test)
83
92
  end
84
93
  end
85
94
 
86
- def build_note(slow_test)
87
- rows = note_header + slow_test_rows(slow_test) + ["\n</details>"]
95
+ def build_note(slow_tests)
96
+ rows = note_header
97
+ rows << "\n#{handbook_guidance}" if slow_tests.any? { |t| feature_spec?(t) }
98
+ rows += [
99
+ "<details><summary>Click to expand</summary>\n",
100
+ '| Job | File | Name | Duration | Expected duration |',
101
+ '| --- | --- | --- | --- | --- |'
102
+ ]
88
103
 
89
- rows.join("\n")
104
+ (rows + slow_test_rows(slow_tests) + ["\n</details>"]).join("\n")
90
105
  end
91
106
 
92
107
  def note_comment_includes_slow_test?(gitlab_note, slow_test)
@@ -8,34 +8,6 @@ module GitlabQuality
8
8
  class Config
9
9
  include Singleton
10
10
 
11
- # GCS client configuration object used to push metrics as json file to gcs bucket
12
- #
13
- class GCS
14
- def initialize(project_id:, credentials:, bucket_name:, metrics_file_name:, dry_run: false)
15
- @project_id = project_id
16
- @credentials = credentials
17
- @bucket_name = bucket_name
18
- @metrics_file_name = metrics_file_name
19
- @dry_run = dry_run
20
- end
21
-
22
- attr_reader :project_id, :credentials, :bucket_name, :metrics_file_name, :dry_run
23
- end
24
-
25
- # ClickHouse client configuration object
26
- #
27
- class ClickHouse
28
- def initialize(url:, database:, table_name:, username:, password:)
29
- @url = url
30
- @database = database
31
- @table_name = table_name
32
- @username = username
33
- @password = password
34
- end
35
-
36
- attr_reader :url, :database, :table_name, :username, :password
37
- end
38
-
39
11
  class << self
40
12
  def configuration
41
13
  Config.instance
@@ -46,8 +18,13 @@ module GitlabQuality
46
18
  end
47
19
  end
48
20
 
49
- attr_reader :gcs_config, :clickhouse_config, :initial_run
50
- attr_accessor :run_type
21
+ attr_reader :initial_run
22
+ attr_accessor :run_type,
23
+ :clickhouse_url,
24
+ :clickhouse_database,
25
+ :clickhouse_table_name,
26
+ :clickhouse_username,
27
+ :clickhouse_password
51
28
  attr_writer :extra_rspec_metadata_keys,
52
29
  :skip_record_proc,
53
30
  :test_retried_proc,
@@ -55,23 +32,7 @@ module GitlabQuality
55
32
  :spec_file_path_prefix,
56
33
  :logger
57
34
 
58
- # rubocop:disable Style/TrivialAccessors -- allows documenting that setting config enables the export as well as document input class type
59
-
60
- # Enable metrics export to gcs bucket by setting configuration object
61
- #
62
- # @param config [Config::GCS]
63
- # @return [GCS]
64
- def gcs_config=(config)
65
- @gcs_config = config
66
- end
67
-
68
- # Enable metrics export to clickhouse by setting configuration object
69
- #
70
- # @param config [Config::ClickHouse]
71
- # @return [ClickHouse]
72
- def clickhouse_config=(config)
73
- @clickhouse_config = config
74
- end
35
+ # rubocop:disable Style/TrivialAccessors -- allows adding documentation for extra_metadata_columns
75
36
 
76
37
  # Additional columns to be created in the table if initial_run setup is used
77
38
  # Columns should be defined in the format used for ALTER TABLE query, example;
@@ -88,6 +49,15 @@ module GitlabQuality
88
49
 
89
50
  # rubocop:enable Style/TrivialAccessors
90
51
 
52
+ # Whether ClickHouse export is configured
53
+ #
54
+ # Export is considered enabled when all required attributes are set
55
+ #
56
+ # @return [Boolean]
57
+ def clickhouse_configured?
58
+ [clickhouse_url, clickhouse_database, clickhouse_table_name, clickhouse_username, clickhouse_password].none?(&:blank?)
59
+ end
60
+
91
61
  # Marks execution as initial run and performs setup tasks before running tests, like creating database in ClickHouse
92
62
  #
93
63
  # @return [Boolean]
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module TestMetricsExporter
8
+ class ConfigHelper
9
+ REQUIRED_CLICKHOUSE_ENV_VARS = %w[
10
+ GLCI_DA_CLICKHOUSE_URL
11
+ GLCI_CLICKHOUSE_METRICS_USERNAME
12
+ GLCI_CLICKHOUSE_METRICS_PASSWORD
13
+ GLCI_CLICKHOUSE_METRICS_DB
14
+ GLCI_CLICKHOUSE_METRICS_TABLE
15
+ GLCI_CLICKHOUSE_SHARED_DB
16
+ ].freeze
17
+
18
+ class << self
19
+ def configure!(run_type = test_run_type)
20
+ return unless ENV.fetch("CI", nil) && ENV["GLCI_EXPORT_TEST_METRICS"] == "true" && run_type
21
+
22
+ RSpec.configure do |rspec_config|
23
+ next if rspec_config.dry_run?
24
+
25
+ Config.configure do |exporter_config|
26
+ self.logger = exporter_config.logger
27
+ next warn_missing_clickhouse_variables unless clickhouse_env_vars_present?
28
+
29
+ yield(exporter_config) if block_given?
30
+ configure_exporter!(exporter_config, run_type)
31
+
32
+ rspec_config.add_formatter Formatter
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_writer :logger
40
+
41
+ def logger
42
+ @logger ||= Logger.new($stdout)
43
+ end
44
+
45
+ def clickhouse_env_vars_present?
46
+ REQUIRED_CLICKHOUSE_ENV_VARS.all? { |var| ENV.fetch(var, nil) && !ENV[var].empty? }
47
+ end
48
+
49
+ def owner_records
50
+ @owner_records ||= GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(
51
+ database: ENV.fetch("GLCI_CLICKHOUSE_SHARED_DB", nil),
52
+ url: clickhouse_url,
53
+ username: clickhouse_username,
54
+ password: clickhouse_password
55
+ ).owner_records
56
+ rescue StandardError => e
57
+ logger.error("Failed to retrieve owner data: #{e}")
58
+ @owner_records = {}
59
+ end
60
+
61
+ def configure_exporter!(config, run_type)
62
+ config.run_type = run_type
63
+ config.custom_metrics_proc = custom_metrics_proc
64
+
65
+ configure_clickhouse!(config)
66
+ end
67
+
68
+ def configure_clickhouse!(config)
69
+ config.clickhouse_database = ENV.fetch("GLCI_CLICKHOUSE_METRICS_DB", nil)
70
+ config.clickhouse_table_name = ENV.fetch("GLCI_CLICKHOUSE_METRICS_TABLE", nil)
71
+ config.clickhouse_url = clickhouse_url
72
+ config.clickhouse_username = clickhouse_username
73
+ config.clickhouse_password = clickhouse_password
74
+ end
75
+
76
+ def warn_missing_clickhouse_variables
77
+ missing = REQUIRED_CLICKHOUSE_ENV_VARS.reject { |var| ENV.fetch(var, nil) && !ENV[var].empty? }
78
+ logger.warn("Test metrics export is enabled but missing environment variables: #{missing.join(', ')}")
79
+ end
80
+
81
+ def custom_metrics_proc
82
+ proc do |example|
83
+ feature_category = example.metadata[:feature_category]
84
+
85
+ owners = if feature_category.blank?
86
+ logger.warn("Example '#{example.description}' is missing feature category metadata!")
87
+ {}
88
+ elsif unowned?(feature_category)
89
+ # currently will default to shared or tooling
90
+ { group: feature_category, stage: feature_category, section: feature_category }
91
+ else
92
+ owner_records.fetch(feature_category.to_s, {}).tap do |o|
93
+ logger.warn("Feature category '#{feature_category}' has no owner data") if o.empty?
94
+ end
95
+ end
96
+
97
+ { pipeline_type: pipeline_type, ci_pipeline_id: ci_pipeline_id, **owners }
98
+ end
99
+ end
100
+
101
+ def default_branch?
102
+ ENV["CI_COMMIT_REF_NAME"] == ENV["CI_DEFAULT_BRANCH"]
103
+ end
104
+
105
+ def pipeline_type
106
+ @pipeline_type ||= if default_branch? && ENV["SCHEDULE_TYPE"].present?
107
+ "default_branch_scheduled_pipeline"
108
+ elsif default_branch?
109
+ "default_branch_pipeline"
110
+ elsif ENV["CI_COMMIT_REF_NAME"]&.match?(/^[\d-]+-stable-ee$/)
111
+ "stable_branch_pipeline"
112
+ elsif ENV["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]&.match?(/^[\d-]+-stable-ee$/)
113
+ "backport_merge_request_pipeline"
114
+ elsif ENV["CI_MERGE_REQUEST_IID"].present?
115
+ "merge_request_pipeline"
116
+ elsif ENV["CI_PIPELINE_SOURCE"] == "pipeline"
117
+ "downstream_pipeline"
118
+ else
119
+ "unknown"
120
+ end
121
+ end
122
+
123
+ def unowned?(feature_category)
124
+ GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable::KNOWN_UNOWNED.include?(
125
+ feature_category.to_s
126
+ )
127
+ end
128
+
129
+ def test_run_type
130
+ @run_type ||= ENV.fetch("GLCI_TEST_METRICS_RUN_TYPE", nil)
131
+ end
132
+
133
+ def clickhouse_url
134
+ ENV.fetch("GLCI_DA_CLICKHOUSE_URL", nil)
135
+ end
136
+
137
+ def clickhouse_username
138
+ ENV.fetch("GLCI_CLICKHOUSE_METRICS_USERNAME", nil)
139
+ end
140
+
141
+ def clickhouse_password
142
+ ENV.fetch("GLCI_CLICKHOUSE_METRICS_PASSWORD", nil)
143
+ end
144
+
145
+ def ci_pipeline_id
146
+ (ENV["PARENT_PIPELINE_ID"] || ENV.fetch("CI_PIPELINE_ID", nil)).to_i
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -16,7 +16,7 @@ module GitlabQuality
16
16
  return unless config.initial_run
17
17
 
18
18
  logger.info("#{LOG_PREFIX} Running initial setup for metrics export")
19
- raise "Initial setup is enabled, but clickhouse configuration is missing!" unless clickhouse_config
19
+ raise "Initial setup is enabled, but clickhouse configuration is missing!" unless config.clickhouse_configured?
20
20
 
21
21
  create_clickhouse_metrics_table
22
22
  rescue StandardError => e
@@ -32,14 +32,11 @@ module GitlabQuality
32
32
  end
33
33
  return logger.warn("#{LOG_PREFIX} No test execution records found, metrics will not be exported!") if data.empty?
34
34
 
35
- push_to_gcs(data)
36
35
  push_to_clickhouse(data)
37
36
  end
38
37
 
39
38
  private
40
39
 
41
- delegate :gcs_config, :clickhouse_config, to: :config
42
-
43
40
  # Single common timestamp for all exported example metrics to keep data points consistently grouped
44
41
  #
45
42
  # @return [String]
@@ -50,27 +47,14 @@ module GitlabQuality
50
47
  @time = (ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc).strftime('%Y-%m-%dT%H:%M:%S.%6N')
51
48
  end
52
49
 
53
- # Push data to gcs
54
- #
55
- # @param data [Array]
56
- # @return [void]
57
- def push_to_gcs(data)
58
- return logger.debug("#{LOG_PREFIX} GCS configuration missing, skipping gcs export!") unless gcs_config
59
-
60
- gcs_client.put_object(gcs_config.bucket_name, gcs_config.metrics_file_name, data.to_json)
61
- logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to GCS bucket!")
62
- rescue StandardError => e
63
- logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to GCS: #{e.message}")
64
- end
65
-
66
50
  # Push data to clickhouse
67
51
  #
68
52
  # @param data [Array<Hash>]
69
53
  # @return [void]
70
54
  def push_to_clickhouse(data)
71
- return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless clickhouse_config
55
+ return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless config.clickhouse_configured?
72
56
 
73
- clickhouse_client.insert_json_data(clickhouse_config.table_name, data)
57
+ clickhouse_client.insert_json_data(config.clickhouse_table_name, data)
74
58
  logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to ClickHouse!")
75
59
  rescue StandardError => e
76
60
  logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to ClickHouse: #{e.message}")
@@ -23,32 +23,21 @@ module GitlabQuality
23
23
  # @return [ClickHouse::Client]
24
24
  def clickhouse_client
25
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,
26
+ url: config.clickhouse_url,
27
+ database: config.clickhouse_database,
28
+ username: config.clickhouse_username,
29
+ password: config.clickhouse_password,
30
30
  logger: logger
31
31
  )
32
32
  end
33
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
34
  # Create table for metrics export using current ClickHouse configuration
46
35
  #
47
36
  # This method is mostly for schema documentation but it can be used together with initial_run! method in
48
37
  #
49
38
  # @return [void]
50
39
  def create_clickhouse_metrics_table
51
- table_name = config.clickhouse_config.table_name
40
+ table_name = config.clickhouse_table_name
52
41
 
53
42
  clickhouse_client.query(<<~SQL)
54
43
  CREATE TABLE IF NOT EXISTS #{table_name}
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.11.0"
5
+ VERSION = "3.13.0"
6
6
  end
7
7
  end
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: 3.11.0
4
+ version: 3.13.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: 2026-04-15 00:00:00.000000000 Z
11
+ date: 2026-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -302,6 +302,26 @@ dependencies:
302
302
  - - "~>"
303
303
  - !ruby/object:Gem::Version
304
304
  version: '5.0'
305
+ - !ruby/object:Gem::Dependency
306
+ name: http-cookie
307
+ requirement: !ruby/object:Gem::Requirement
308
+ requirements:
309
+ - - ">="
310
+ - !ruby/object:Gem::Version
311
+ version: '1.0'
312
+ - - "<"
313
+ - !ruby/object:Gem::Version
314
+ version: 1.1.1
315
+ type: :runtime
316
+ prerelease: false
317
+ version_requirements: !ruby/object:Gem::Requirement
318
+ requirements:
319
+ - - ">="
320
+ - !ruby/object:Gem::Version
321
+ version: '1.0'
322
+ - - "<"
323
+ - !ruby/object:Gem::Version
324
+ version: 1.1.1
305
325
  - !ruby/object:Gem::Dependency
306
326
  name: nokogiri
307
327
  requirement: !ruby/object:Gem::Requirement
@@ -569,6 +589,7 @@ files:
569
589
  - lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
570
590
  - lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
571
591
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb
592
+ - lib/gitlab_quality/test_tooling/test_metrics_exporter/config_helper.rb
572
593
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb
573
594
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb
574
595
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb