gitlab_quality-test_tooling 2.16.0 → 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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +30 -28
  5. data/exe/epic-readiness-notification +58 -0
  6. data/exe/relate-failure-issue +5 -0
  7. data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
  8. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  9. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  10. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  11. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  12. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
  13. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  14. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
  15. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
  16. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
  17. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
  18. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
  19. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  20. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  21. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  22. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  23. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  24. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  25. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  26. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
  27. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  28. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  29. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  30. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
  31. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  32. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  33. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +134 -5
  34. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
  35. data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
  36. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  37. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  38. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  39. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  40. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
  41. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
  42. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +57 -36
  43. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +124 -80
  44. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +94 -0
  45. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
  46. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  47. data/lib/gitlab_quality/test_tooling.rb +3 -0
  48. metadata +70 -55
  49. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  50. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  51. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/influxdb_tools.rb +0 -33
@@ -8,6 +8,34 @@ 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
+
11
39
  class << self
12
40
  def configuration
13
41
  Config.instance
@@ -18,25 +46,97 @@ module GitlabQuality
18
46
  end
19
47
  end
20
48
 
21
- attr_accessor :influxdb_url,
22
- :influxdb_token,
23
- :influxdb_bucket,
24
- :gcs_bucket,
25
- :gcs_project_id,
26
- :gcs_credentials,
27
- :gcs_metrics_file_name,
28
- :test_metric_file_name,
29
- :run_type
49
+ attr_reader :gcs_config, :clickhouse_config, :initial_run
50
+ attr_accessor :run_type
51
+ attr_writer :extra_rspec_metadata_keys,
52
+ :skip_record_proc,
53
+ :test_retried_proc,
54
+ :custom_metrics_proc,
55
+ :logger
56
+
57
+ # rubocop:disable Style/TrivialAccessors -- allows documenting that setting config enables the export as well as document input class type
58
+
59
+ # Enable metrics export to gcs bucket by setting configuration object
60
+ #
61
+ # @param config [Config::GCS]
62
+ # @return [GCS]
63
+ def gcs_config=(config)
64
+ @gcs_config = config
65
+ end
66
+
67
+ # Enable metrics export to clickhouse by setting configuration object
68
+ #
69
+ # @param config [Config::ClickHouse]
70
+ # @return [ClickHouse]
71
+ def clickhouse_config=(config)
72
+ @clickhouse_config = config
73
+ end
30
74
 
31
- attr_writer :custom_keys_tags,
32
- :custom_keys_fields
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
+
88
+ # rubocop:enable Style/TrivialAccessors
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
+
104
+ # Extra rspec metadata keys to include in exported metrics
105
+ #
106
+ # @return [Array<Symbol>]
107
+ def extra_rspec_metadata_keys
108
+ @extra_rspec_metadata_keys ||= []
109
+ end
110
+
111
+ # A lambda that determines whether to skip recording a test result
112
+ #
113
+ # This is useful when you would want to skip initial failure when retrying specs is set up in a separate process
114
+ # and you want to avoid duplicate records
115
+ #
116
+ # @return [Proc]
117
+ def skip_record_proc
118
+ @skip_record_proc ||= ->(_example) { false }
119
+ end
120
+
121
+ # A lambda that determines whether a test was retried or not
122
+ #
123
+ # @return [Proc]
124
+ def test_retried_proc
125
+ @test_retried_proc ||= ->(_example) { false }
126
+ end
33
127
 
34
- def custom_keys_tags
35
- @custom_keys_tags || []
128
+ # A lambda that return hash with additional custom metrics
129
+ #
130
+ # @return [Proc]
131
+ def custom_metrics_proc
132
+ @custom_metrics_proc ||= ->(_example) { {} }
36
133
  end
37
134
 
38
- def custom_keys_fields
39
- @custom_keys_fields || []
135
+ # Logger instance
136
+ #
137
+ # @return [Logger]
138
+ def logger
139
+ @logger ||= Logger.new($stdout, level: Logger::INFO)
40
140
  end
41
141
  end
42
142
  end
@@ -1,55 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rspec/core/formatters/base_formatter"
4
+
3
5
  module GitlabQuality
4
6
  module TestTooling
5
7
  module TestMetricsExporter
6
8
  class Formatter < RSpec::Core::Formatters::BaseFormatter
7
- RSpec::Core::Formatters.register(self, :stop)
9
+ include Utils
10
+
11
+ RSpec::Core::Formatters.register(self, *[:stop, Utils.config.initial_run ? :start : nil].compact)
12
+
13
+ LOG_PREFIX = "[MetricsExporter]"
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
8
21
 
9
22
  def stop(notification)
10
- setup_test_metrics_exporter(notification.examples)
11
-
12
- log_test_metrics.push_test_metrics(
13
- custom_keys_tags: config.custom_keys_tags,
14
- custom_keys_fields: config.custom_keys_fields
15
- )
16
-
17
- log_test_metrics.save_test_metrics(
18
- file_name: config.test_metric_file_name,
19
- custom_keys_tags: config.custom_keys_tags,
20
- custom_keys_fields: config.custom_keys_fields
21
- )
23
+ logger.debug("#{LOG_PREFIX} Starting test metrics export")
24
+ data = notification.examples.filter_map do |example|
25
+ next if config.skip_record_proc.call(example)
26
+
27
+ TestMetrics.new(example, time).data
28
+ end
29
+ return logger.warn("#{LOG_PREFIX} No test execution records found, metrics will not be exported!") if data.empty?
30
+
31
+ push_to_gcs(data)
32
+ push_to_clickhouse(data)
22
33
  end
23
34
 
24
35
  private
25
36
 
26
- attr_reader :log_test_metrics
37
+ delegate :gcs_config, :clickhouse_config, to: :config
38
+
39
+ # Single common timestamp for all exported example metrics to keep data points consistently grouped
40
+ #
41
+ # @return [Time]
42
+ def time
43
+ return @time if @time
44
+
45
+ ci_created_at = Runtime::Env.ci_pipeline_created_at
46
+ @time = ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc
47
+ end
48
+
49
+ # Push data to gcs
50
+ #
51
+ # @param data [Array]
52
+ # @return [void]
53
+ def push_to_gcs(data)
54
+ return logger.debug("#{LOG_PREFIX} GCS configuration missing, skipping gcs export!") unless gcs_config
27
55
 
28
- def config
29
- Config.configuration
56
+ gcs_client.put_object(gcs_config.bucket_name, gcs_config.metrics_file_name, data.to_json)
57
+ logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to GCS bucket!")
58
+ rescue StandardError => e
59
+ logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to GCS: #{e.message}")
30
60
  end
31
61
 
32
- # rubocop:disable Metrics/AbcSize
33
- def setup_test_metrics_exporter(examples)
34
- @log_test_metrics = LogTestMetrics.new(
35
- examples: examples,
36
- run_type: config.run_type
37
- )
38
-
39
- @log_test_metrics.configure_influxdb_client(
40
- influxdb_url: config.influxdb_url,
41
- influxdb_token: config.influxdb_token,
42
- influxdb_bucket: config.influxdb_bucket
43
- )
44
-
45
- @log_test_metrics.configure_gcs_client(
46
- gcs_bucket: config.gcs_bucket,
47
- gcs_project_id: config.gcs_project_id,
48
- gcs_credentials: config.gcs_credentials,
49
- gcs_metrics_file_name: config.gcs_metrics_file_name
50
- )
62
+ # Push data to clickhouse
63
+ #
64
+ # @param data [Array<Hash>]
65
+ # @return [void]
66
+ def push_to_clickhouse(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)
70
+ logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to ClickHouse!")
71
+ rescue StandardError => e
72
+ logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to ClickHouse: #{e.message}")
51
73
  end
52
- # rubocop:enable Metrics/AbcSize
53
74
  end
54
75
  end
55
76
  end
@@ -5,131 +5,149 @@ require 'time'
5
5
  module GitlabQuality
6
6
  module TestTooling
7
7
  module TestMetricsExporter
8
- module TestMetrics
9
- # Single common timestamp for all exported example metrics to keep data points consistently grouped
10
- #
11
- # @return [Time]
12
- def time
13
- return @time if defined?(@time)
14
-
15
- created_at = Time.strptime(env('CI_PIPELINE_CREATED_AT'), '%Y-%m-%dT%H:%M:%S%z') if env('CI_PIPELINE_CREATED_AT')
16
- @time = Time.parse((created_at || Time.now).utc.strftime('%Y-%m-%d %H:%M:%S %z'))
8
+ class TestMetrics
9
+ def initialize(example, timestamp)
10
+ @example = example
11
+ @timestamp = timestamp
17
12
  end
18
13
 
19
- # rubocop:disable Metrics/AbcSize
20
- # Metrics tags
14
+ # Test data hash
21
15
  #
22
- # @param [RSpec::Core::Example] example
23
- # @param [Array<String>] custom_keys
24
- # @param [String]
25
16
  # @return [Hash]
26
- def tags(example, custom_keys, run_type)
17
+ def data
27
18
  {
28
- name: example.full_description,
29
- file_path: example.metadata[:file_path].sub(/\A./, ''),
30
- status: status(example),
31
- quarantined: quarantined(example),
32
- job_name: job_name,
33
- merge_request: merge_request,
34
- run_type: run_type,
35
- feature_category: example.metadata[:feature_category],
36
- product_group: example.metadata[:product_group],
37
- exception_class: example.execution_result.exception&.class&.to_s,
38
- **custom_metrics(example.metadata, custom_keys)
19
+ time: timestamp,
20
+ **rspec_metrics,
21
+ **ci_metrics,
22
+ **custom_metrics
39
23
  }.compact
40
24
  end
41
- # rubocop:enable Metrics/AbcSize
42
25
 
43
- # Metrics fields
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
44
38
  #
45
- # @param [RSpec::Core::Example] example
46
- # @param [Array<String>] custom_keys
47
39
  # @return [Hash]
48
- def fields(example, custom_keys)
40
+ def rspec_metrics # rubocop:disable Metrics/AbcSize
49
41
  {
50
- id: example.id,
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,
51
47
  run_time: (example.execution_result.run_time * 1000).round,
52
- job_url: Runtime::Env.ci_job_url,
53
- pipeline_url: env('CI_PIPELINE_URL'),
54
- pipeline_id: env('CI_PIPELINE_ID'),
55
- job_id: env('CI_JOB_ID'),
56
- merge_request_iid: merge_request_iid,
57
- failure_exception: example.execution_result.exception.to_s.delete("\n"),
58
- **custom_metrics(example.metadata, custom_keys)
59
- }.compact
48
+ location: example_location,
49
+ exception_class: exception_class,
50
+ failure_exception: failure_exception,
51
+ quarantined: quarantined?,
52
+ feature_category: example.metadata[:feature_category] || "",
53
+ test_retried: config.test_retried_proc.call(example)
54
+ }
60
55
  end
61
56
 
62
- # Return a more detailed status
57
+ # CI related metrics
63
58
  #
64
- # - if test is failed or pending, return rspec status
65
- # - if test passed but had more than 1 attempt, consider test flaky
66
- #
67
- # @param [RSpec::Core::Example] example
68
- # @return [Symbol]
69
- def status(example)
70
- rspec_status = example.execution_result.status
71
- return rspec_status if [:pending, :failed].include?(rspec_status)
72
-
73
- retry_attempts(example.metadata).positive? ? :flaky : :passed
59
+ # @return [Hash]
60
+ def ci_metrics
61
+ {
62
+ ci_project_id: env("CI_PROJECT_ID")&.to_i,
63
+ ci_project_path: env("CI_PROJECT_PATH"),
64
+ ci_job_name: ci_job_name,
65
+ ci_job_id: env('CI_JOB_ID')&.to_i,
66
+ ci_pipeline_id: env('CI_PIPELINE_ID')&.to_i,
67
+ ci_merge_request_iid: (env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID'))&.to_i,
68
+ ci_branch: env("CI_COMMIT_REF_NAME"),
69
+ ci_target_branch: env("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")
70
+ }
74
71
  end
75
72
 
76
- # Retry attempts
73
+ # Additional custom metrics
77
74
  #
78
- # @param [Hash] example
79
- # @return [Integer]
80
- def retry_attempts(metadata)
81
- metadata[:retry_attempts] || 0
75
+ # @return [Hash]
76
+ def custom_metrics
77
+ metrics = example.metadata
78
+ .slice(*config.extra_rspec_metadata_keys)
79
+ .merge(config.custom_metrics_proc.call(example))
80
+
81
+ metrics.each_with_object({}) do |(k, value), custom_metrics|
82
+ custom_metrics[k.to_sym] = metrics_value(value)
83
+ end
82
84
  end
83
85
 
84
86
  # Checks if spec is quarantined
85
87
  #
86
- # @param [RSpec::Core::Example] example
87
88
  # @return [String]
88
- def quarantined(example)
89
- return "false" unless example.metadata.key?(:quarantine)
89
+ def quarantined?
90
+ return false unless example.metadata.key?(:quarantine)
90
91
 
91
92
  # if quarantine key is present and status is pending, consider it quarantined
92
- (example.execution_result.status == :pending).to_s
93
+ example.execution_result.status == :pending
93
94
  end
94
95
 
95
96
  # Base ci job name
96
97
  #
97
98
  # @return [String]
98
- def job_name
99
- @job_name ||= Runtime::Env.ci_job_name&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
99
+ def ci_job_name
100
+ env("CI_JOB_NAME")&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
100
101
  end
101
102
 
102
- # Check if it is a merge request execution
103
+ # Example location
103
104
  #
104
105
  # @return [String]
105
- def merge_request
106
- (!!merge_request_iid).to_s
106
+ def example_location
107
+ return @example_location if @example_location
108
+
109
+ # ensures that location will be correct even in case of shared examples
110
+ file = example
111
+ .metadata
112
+ .fetch(:shared_group_inclusion_backtrace)
113
+ .last
114
+ &.formatted_inclusion_location
115
+
116
+ return without_relative_path(example.location) unless file
117
+
118
+ @example_location = without_relative_path(file)
107
119
  end
108
120
 
109
- # Merge request iid
121
+ # File path based on actual test location, not shared example location
110
122
  #
111
123
  # @return [String]
112
- def merge_request_iid
113
- env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID')
124
+ def file_path
125
+ @file_path ||= example_location.gsub(/:\d+$/, "")
114
126
  end
115
127
 
116
- # Custom test metrics
128
+ # Failure exception class
117
129
  #
118
- # @param [Hash] metadata
119
- # @param [Array] array of custom metrics keys
120
- # @return [Hash]
121
- def custom_metrics(metadata, custom_keys)
122
- return {} if custom_keys.nil?
130
+ # @return [String]
131
+ def exception_class
132
+ example.execution_result.exception&.class&.to_s
133
+ end
123
134
 
124
- custom_metrics = {}
125
- custom_keys.each do |k|
126
- value = metadata[k.to_sym]
127
- v = value.is_a?(Numeric) || value.nil? ? value : value.to_s
135
+ # Truncated exception stacktrace
136
+ #
137
+ # @return [String]
138
+ def failure_exception
139
+ example.execution_result.exception.then do |exception|
140
+ next unless exception
128
141
 
129
- custom_metrics[k.to_sym] = v
142
+ exception.to_s.tr("\n", " ").slice(0, 1000)
130
143
  end
144
+ end
131
145
 
132
- custom_metrics
146
+ # Test run type | suite name
147
+ #
148
+ # @return [String]
149
+ def run_type
150
+ config.run_type || ci_job_name || "unknown"
133
151
  end
134
152
 
135
153
  # Return non empty environment variable value
@@ -141,6 +159,32 @@ module GitlabQuality
141
159
 
142
160
  ENV.fetch(name)
143
161
  end
162
+
163
+ # Metrics value cast to a valid type
164
+ #
165
+ # @param value [Object]
166
+ # @return [Object]
167
+ def metrics_value(value)
168
+ return value if value.is_a?(Numeric) || value.is_a?(String) || bool?(value) || value.nil?
169
+
170
+ value.to_s
171
+ end
172
+
173
+ # Value is a true or false
174
+ #
175
+ # @param val [Object]
176
+ # @return [Boolean]
177
+ def bool?(val)
178
+ [true, false].include?(val)
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
144
188
  end
145
189
  end
146
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
@@ -73,6 +73,10 @@ module GitlabQuality
73
73
  product_group != ''
74
74
  end
75
75
 
76
+ def feature_category?
77
+ feature_category && !feature_category.empty?
78
+ end
79
+
76
80
  def failure_issue
77
81
  report['failure_issue']
78
82
  end
@@ -115,7 +119,7 @@ module GitlabQuality
115
119
  end
116
120
 
117
121
  def file_base_url
118
- @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}/"
119
123
  end
120
124
 
121
125
  def test_file_link
@@ -154,7 +158,7 @@ module GitlabQuality
154
158
  attr_reader :token, :project, :ref
155
159
 
156
160
  def mapped_project
157
- if project == 'gitlab-org/quality/e2e-test-issues'
161
+ if ['gitlab-org/quality/e2e-test-issues', 'gitlab-org/quality/test-failure-issues'].include?(project)
158
162
  'gitlab-org/gitlab'
159
163
  else
160
164
  project
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "2.16.0"
5
+ VERSION = "2.21.0"
6
6
  end
7
7
  end
@@ -2,11 +2,14 @@
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
8
9
  Error = Class.new(StandardError)
9
10
  loader = Zeitwerk::Loader.new
11
+ loader.push_dir(__dir__.to_s, namespace: GitlabQuality)
12
+ loader.ignore("#{__dir__}/test_tooling.rb")
10
13
  loader.push_dir("#{__dir__}/test_tooling", namespace: GitlabQuality::TestTooling)
11
14
  loader.ignore("#{__dir__}/test_tooling/version.rb")
12
15