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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/Gemfile.lock +30 -28
- data/exe/epic-readiness-notification +58 -0
- data/exe/relate-failure-issue +5 -0
- data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
- data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
- data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
- data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +134 -5
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +57 -36
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +124 -80
- 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 +6 -2
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +3 -0
- metadata +70 -55
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
- 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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
:
|
|
25
|
-
:
|
|
26
|
-
:
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
#
|
|
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
|
|
17
|
+
def data
|
|
27
18
|
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
#
|
|
57
|
+
# CI related metrics
|
|
63
58
|
#
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
#
|
|
73
|
+
# Additional custom metrics
|
|
77
74
|
#
|
|
78
|
-
# @
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
89
|
-
return
|
|
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
|
-
|
|
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
|
|
99
|
-
|
|
99
|
+
def ci_job_name
|
|
100
|
+
env("CI_JOB_NAME")&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
|
|
100
101
|
end
|
|
101
102
|
|
|
102
|
-
#
|
|
103
|
+
# Example location
|
|
103
104
|
#
|
|
104
105
|
# @return [String]
|
|
105
|
-
def
|
|
106
|
-
|
|
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
|
-
#
|
|
121
|
+
# File path based on actual test location, not shared example location
|
|
110
122
|
#
|
|
111
123
|
# @return [String]
|
|
112
|
-
def
|
|
113
|
-
|
|
124
|
+
def file_path
|
|
125
|
+
@file_path ||= example_location.gsub(/:\d+$/, "")
|
|
114
126
|
end
|
|
115
127
|
|
|
116
|
-
#
|
|
128
|
+
# Failure exception class
|
|
117
129
|
#
|
|
118
|
-
# @
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
142
|
+
exception.to_s.tr("\n", " ").slice(0, 1000)
|
|
130
143
|
end
|
|
144
|
+
end
|
|
131
145
|
|
|
132
|
-
|
|
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/#{
|
|
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
|
|
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,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
|
|