gitlab_quality-test_tooling 2.16.0 → 2.25.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.
Files changed (67) 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/README.md +1 -1
  6. data/exe/epic-readiness-notification +58 -0
  7. data/exe/post-to-slack +4 -0
  8. data/exe/relate-failure-issue +9 -0
  9. data/exe/test-coverage +113 -0
  10. data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
  11. data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +77 -0
  12. data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +158 -0
  13. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +62 -0
  14. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +109 -0
  15. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +73 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +82 -0
  17. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +91 -0
  18. data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
  19. data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
  20. data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
  21. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  22. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  23. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  24. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  25. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
  26. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  27. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
  28. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
  29. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
  30. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
  31. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
  32. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
  33. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  34. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  35. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  36. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  37. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  38. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  39. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  40. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
  41. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  42. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  43. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  44. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
  45. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  46. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  47. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
  48. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
  49. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
  50. data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
  51. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
  52. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  53. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  54. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  55. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  56. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
  57. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
  58. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
  59. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +125 -80
  60. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +95 -0
  61. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
  62. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  63. data/lib/gitlab_quality/test_tooling.rb +3 -0
  64. metadata +82 -55
  65. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  66. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  67. 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,80 @@
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, :start, :stop)
12
+
13
+ LOG_PREFIX = "[MetricsExporter]"
14
+
15
+ def start(_notification)
16
+ return unless config.initial_run
17
+
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
20
+
21
+ create_clickhouse_metrics_table
22
+ rescue StandardError => e
23
+ logger.error("#{LOG_PREFIX} Error occurred during initial setup: #{e.message}")
24
+ end
8
25
 
9
26
  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
- )
27
+ logger.debug("#{LOG_PREFIX} Starting test metrics export")
28
+ data = notification.examples.filter_map do |example|
29
+ next if config.skip_record_proc.call(example)
30
+
31
+ TestMetrics.new(example, time).data
32
+ end
33
+ return logger.warn("#{LOG_PREFIX} No test execution records found, metrics will not be exported!") if data.empty?
34
+
35
+ push_to_gcs(data)
36
+ push_to_clickhouse(data)
22
37
  end
23
38
 
24
39
  private
25
40
 
26
- attr_reader :log_test_metrics
41
+ delegate :gcs_config, :clickhouse_config, to: :config
27
42
 
28
- def config
29
- Config.configuration
43
+ # Single common timestamp for all exported example metrics to keep data points consistently grouped
44
+ #
45
+ # @return [String]
46
+ def time
47
+ return @time if @time
48
+
49
+ ci_created_at = Runtime::Env.ci_pipeline_created_at
50
+ @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')
30
51
  end
31
52
 
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
- )
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
+ # Push data to clickhouse
67
+ #
68
+ # @param data [Array<Hash>]
69
+ # @return [void]
70
+ def push_to_clickhouse(data)
71
+ return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless clickhouse_config
72
+
73
+ clickhouse_client.insert_json_data(clickhouse_config.table_name, data)
74
+ logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to ClickHouse!")
75
+ rescue StandardError => e
76
+ logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to ClickHouse: #{e.message}")
51
77
  end
52
- # rubocop:enable Metrics/AbcSize
53
78
  end
54
79
  end
55
80
  end
@@ -5,131 +5,150 @@ 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
+ timestamp: 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
+ run_type: run_type
55
+ }
60
56
  end
61
57
 
62
- # Return a more detailed status
58
+ # CI related metrics
63
59
  #
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
60
+ # @return [Hash]
61
+ def ci_metrics
62
+ {
63
+ ci_project_id: env("CI_PROJECT_ID")&.to_i,
64
+ ci_project_path: env("CI_PROJECT_PATH"),
65
+ ci_job_name: ci_job_name,
66
+ ci_job_id: env('CI_JOB_ID')&.to_i,
67
+ ci_pipeline_id: env('CI_PIPELINE_ID')&.to_i,
68
+ ci_merge_request_iid: (env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID'))&.to_i,
69
+ ci_branch: env("CI_COMMIT_REF_NAME"),
70
+ ci_target_branch: env("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")
71
+ }
74
72
  end
75
73
 
76
- # Retry attempts
74
+ # Additional custom metrics
77
75
  #
78
- # @param [Hash] example
79
- # @return [Integer]
80
- def retry_attempts(metadata)
81
- metadata[:retry_attempts] || 0
76
+ # @return [Hash]
77
+ def custom_metrics
78
+ metrics = example.metadata
79
+ .slice(*config.extra_rspec_metadata_keys)
80
+ .merge(config.custom_metrics_proc.call(example))
81
+
82
+ metrics.each_with_object({}) do |(k, value), custom_metrics|
83
+ custom_metrics[k.to_sym] = metrics_value(value)
84
+ end
82
85
  end
83
86
 
84
87
  # Checks if spec is quarantined
85
88
  #
86
- # @param [RSpec::Core::Example] example
87
89
  # @return [String]
88
- def quarantined(example)
89
- return "false" unless example.metadata.key?(:quarantine)
90
+ def quarantined?
91
+ return false unless example.metadata.key?(:quarantine)
90
92
 
91
93
  # if quarantine key is present and status is pending, consider it quarantined
92
- (example.execution_result.status == :pending).to_s
94
+ example.execution_result.status == :pending
93
95
  end
94
96
 
95
97
  # Base ci job name
96
98
  #
97
99
  # @return [String]
98
- def job_name
99
- @job_name ||= Runtime::Env.ci_job_name&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
100
+ def ci_job_name
101
+ env("CI_JOB_NAME")&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
100
102
  end
101
103
 
102
- # Check if it is a merge request execution
104
+ # Example location
103
105
  #
104
106
  # @return [String]
105
- def merge_request
106
- (!!merge_request_iid).to_s
107
+ def example_location
108
+ return @example_location if @example_location
109
+
110
+ # ensures that location will be correct even in case of shared examples
111
+ file = example
112
+ .metadata
113
+ .fetch(:shared_group_inclusion_backtrace)
114
+ .last
115
+ &.formatted_inclusion_location
116
+
117
+ return without_relative_path(example.location) unless file
118
+
119
+ @example_location = without_relative_path(file)
107
120
  end
108
121
 
109
- # Merge request iid
122
+ # File path based on actual test location, not shared example location
110
123
  #
111
124
  # @return [String]
112
- def merge_request_iid
113
- env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID')
125
+ def file_path
126
+ @file_path ||= example_location.gsub(/:\d+$/, "")
114
127
  end
115
128
 
116
- # Custom test metrics
129
+ # Failure exception class
117
130
  #
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?
131
+ # @return [String]
132
+ def exception_class
133
+ example.execution_result.exception&.class&.to_s
134
+ end
123
135
 
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
136
+ # Truncated exception stacktrace
137
+ #
138
+ # @return [String]
139
+ def failure_exception
140
+ example.execution_result.exception.then do |exception|
141
+ next unless exception
128
142
 
129
- custom_metrics[k.to_sym] = v
143
+ exception.to_s.tr("\n", " ").slice(0, 1000)
130
144
  end
145
+ end
131
146
 
132
- custom_metrics
147
+ # Test run type | suite name
148
+ #
149
+ # @return [String]
150
+ def run_type
151
+ config.run_type || ci_job_name || "unknown"
133
152
  end
134
153
 
135
154
  # Return non empty environment variable value
@@ -141,6 +160,32 @@ module GitlabQuality
141
160
 
142
161
  ENV.fetch(name)
143
162
  end
163
+
164
+ # Metrics value cast to a valid type
165
+ #
166
+ # @param value [Object]
167
+ # @return [Object]
168
+ def metrics_value(value)
169
+ return value if value.is_a?(Numeric) || value.is_a?(String) || bool?(value) || value.nil?
170
+
171
+ value.to_s
172
+ end
173
+
174
+ # Value is a true or false
175
+ #
176
+ # @param val [Object]
177
+ # @return [Boolean]
178
+ def bool?(val)
179
+ [true, false].include?(val)
180
+ end
181
+
182
+ # Path without leading ./
183
+ #
184
+ # @param path [String]
185
+ # @return [String]
186
+ def without_relative_path(path)
187
+ path.gsub(%r{^\./}, "")
188
+ end
144
189
  end
145
190
  end
146
191
  end
@@ -0,0 +1,95 @@
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
+ run_type LowCardinality(String) DEFAULT 'unknown',
68
+ ci_project_id UInt32,
69
+ ci_job_name LowCardinality(String),
70
+ ci_job_id UInt64,
71
+ ci_pipeline_id UInt64,
72
+ ci_merge_request_iid UInt32 DEFAULT 0,
73
+ ci_project_path LowCardinality(String),
74
+ ci_branch String,
75
+ ci_target_branch LowCardinality(String),
76
+ exception_class String DEFAULT '',
77
+ failure_exception String DEFAULT ''
78
+ )
79
+ ENGINE = MergeTree()
80
+ PARTITION BY toYYYYMM(timestamp)
81
+ ORDER BY (ci_project_path, status, run_type, feature_category, file_path, timestamp, ci_pipeline_id)
82
+ SETTINGS index_granularity = 8192;
83
+ SQL
84
+ return if config.extra_metadata_columns.empty?
85
+
86
+ clickhouse_client.query(
87
+ "ALTER TABLE #{table_name} #{config.extra_metadata_columns.map { |column| "ADD COLUMN IF NOT EXISTS #{column}" }.join(', ')};"
88
+ )
89
+ end
90
+
91
+ module_function :config, :logger, :clickhouse_client
92
+ end
93
+ end
94
+ end
95
+ 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.25.1"
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