gitlab_quality-test_tooling 1.22.0 → 1.24.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/Gemfile.lock +5 -2
- data/lib/gitlab_quality/test_tooling/concerns/influxdb_tools.rb +29 -0
- data/lib/gitlab_quality/test_tooling/concerns/test_metrics.rb +145 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +55 -37
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +17 -41
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +32 -7
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +15 -38
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +31 -6
- data/lib/gitlab_quality/test_tooling/stack_trace_comparator.rb +36 -0
- data/lib/gitlab_quality/test_tooling/test_metric/log_test_metrics.rb +76 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +16 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +41 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '009e41374e42e637a571410da7cd95aa23ab4d5a6f1599f080a0ccea8432b820'
|
4
|
+
data.tar.gz: b81d4043e16bc5da888a102d3765250691acb705bb3edac3762c6baad0fe490f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1d34b6b4862000282c042e25b5f0cd02e23e72dee4307d0eb9d2309a0e5d352dfeeeadab2f5da7d01b91fe798a192bf8f2ed075608e9f73854842d107bef55a
|
7
|
+
data.tar.gz: eb3746a140cb622b9ede27514cb26ea7681b1bafffc5320dd9ce6dce83951db2e752f5458fa7af09906488fd6ef871c589c232794a4bf3d8a6962613cc35e484
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitlab_quality-test_tooling (1.
|
4
|
+
gitlab_quality-test_tooling (1.24.0)
|
5
5
|
activesupport (>= 6.1, < 7.2)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
gitlab (~> 4.19)
|
@@ -130,6 +130,7 @@ GEM
|
|
130
130
|
multi_xml (>= 0.5.2)
|
131
131
|
i18n (1.14.4)
|
132
132
|
concurrent-ruby (~> 1.0)
|
133
|
+
influxdb-client (3.1.0)
|
133
134
|
jaro_winkler (1.5.6)
|
134
135
|
json (2.7.1)
|
135
136
|
kramdown (2.4.0)
|
@@ -312,11 +313,13 @@ PLATFORMS
|
|
312
313
|
ruby
|
313
314
|
|
314
315
|
DEPENDENCIES
|
316
|
+
activesupport (>= 6.1, < 7.2)
|
315
317
|
climate_control (~> 1.2)
|
316
318
|
gitlab-dangerfiles (~> 3.8)
|
317
319
|
gitlab-styles (~> 10.0)
|
318
320
|
gitlab_quality-test_tooling!
|
319
321
|
guard-rspec (~> 4.7)
|
322
|
+
influxdb-client (~> 3.1)
|
320
323
|
lefthook (~> 1.3)
|
321
324
|
pry-byebug (= 3.10.1)
|
322
325
|
rake (~> 13.0)
|
@@ -328,4 +331,4 @@ DEPENDENCIES
|
|
328
331
|
webmock (= 3.7.0)
|
329
332
|
|
330
333
|
BUNDLED WITH
|
331
|
-
2.5.
|
334
|
+
2.5.6
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Concerns
|
6
|
+
module InfluxdbTools
|
7
|
+
# InfluxDb client
|
8
|
+
#
|
9
|
+
# @return [InfluxDB2::Client]
|
10
|
+
def influx_client(url:, token:, bucket:)
|
11
|
+
@influx_client ||= InfluxDB2::Client.new(
|
12
|
+
url || raise('Missing influxdb_url'),
|
13
|
+
token || raise('Missing influxdb_token'),
|
14
|
+
bucket: bucket || raise('Missing influxdb_bucket'),
|
15
|
+
org: "gitlab-qa",
|
16
|
+
precision: InfluxDB2::WritePrecision::NANOSECOND
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Write client
|
21
|
+
#
|
22
|
+
# @return [WriteApi]
|
23
|
+
def write_api(url:, token:, bucket:)
|
24
|
+
@write_api ||= influx_client(url: url, token: token, bucket: bucket).create_write_api
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Concerns
|
6
|
+
module TestMetrics
|
7
|
+
# Single common timestamp for all exported example metrics to keep data points consistently grouped
|
8
|
+
#
|
9
|
+
# @return [Time]
|
10
|
+
def time
|
11
|
+
return @time if defined?(@time)
|
12
|
+
|
13
|
+
created_at = Time.strptime(env('CI_PIPELINE_CREATED_AT'), '%Y-%m-%dT%H:%M:%S%z') if env('CI_PIPELINE_CREATED_AT')
|
14
|
+
@time = (created_at || Time.now).utc.strftime('%Y-%m-%dT%H:%M:%S%z')
|
15
|
+
end
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/AbcSize
|
18
|
+
# Metrics tags
|
19
|
+
#
|
20
|
+
# @param [RSpec::Core::Example] example
|
21
|
+
# @param [Array<String>] custom_keys
|
22
|
+
# @param [String]
|
23
|
+
# @return [Hash]
|
24
|
+
def tags(example, custom_keys, run_type)
|
25
|
+
{
|
26
|
+
name: example.full_description,
|
27
|
+
file_path: example.metadata[:file_path].sub(/\A./, ''),
|
28
|
+
status: status(example),
|
29
|
+
quarantined: quarantined(example),
|
30
|
+
job_name: job_name,
|
31
|
+
merge_request: merge_request,
|
32
|
+
run_type: run_type,
|
33
|
+
stage: example.metadata[:product_stage] || example.metadata[:product_category],
|
34
|
+
product_group: example.metadata[:product_group],
|
35
|
+
exception_class: example.execution_result.exception&.class&.to_s,
|
36
|
+
**custom_metrics(example.metadata, custom_keys)
|
37
|
+
}.compact
|
38
|
+
end
|
39
|
+
# rubocop:enable Metrics/AbcSize
|
40
|
+
|
41
|
+
# Metrics fields
|
42
|
+
#
|
43
|
+
# @param [RSpec::Core::Example] example
|
44
|
+
# @param [Array<String>] custom_keys
|
45
|
+
# @return [Hash]
|
46
|
+
def fields(example, custom_keys)
|
47
|
+
{
|
48
|
+
id: example.id,
|
49
|
+
run_time: (example.execution_result.run_time * 1000).round,
|
50
|
+
job_url: Runtime::Env.ci_job_url,
|
51
|
+
pipeline_url: env('CI_PIPELINE_URL'),
|
52
|
+
pipeline_id: env('CI_PIPELINE_ID'),
|
53
|
+
job_id: env('CI_JOB_ID'),
|
54
|
+
merge_request_iid: merge_request_iid,
|
55
|
+
failure_exception: example.execution_result.exception.to_s.delete("\n"),
|
56
|
+
**custom_metrics(example.metadata, custom_keys)
|
57
|
+
}.compact
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return a more detailed status
|
61
|
+
#
|
62
|
+
# - if test is failed or pending, return rspec status
|
63
|
+
# - if test passed but had more than 1 attempt, consider test flaky
|
64
|
+
#
|
65
|
+
# @param [RSpec::Core::Example] example
|
66
|
+
# @return [Symbol]
|
67
|
+
def status(example)
|
68
|
+
rspec_status = example.execution_result.status
|
69
|
+
return rspec_status if [:pending, :failed].include?(rspec_status)
|
70
|
+
|
71
|
+
retry_attempts(example.metadata).positive? ? :flaky : :passed
|
72
|
+
end
|
73
|
+
|
74
|
+
# Retry attempts
|
75
|
+
#
|
76
|
+
# @param [Hash] example
|
77
|
+
# @return [Integer]
|
78
|
+
def retry_attempts(metadata)
|
79
|
+
metadata[:retry_attempts] || 0
|
80
|
+
end
|
81
|
+
|
82
|
+
# Checks if spec is quarantined
|
83
|
+
#
|
84
|
+
# @param [RSpec::Core::Example] example
|
85
|
+
# @return [String]
|
86
|
+
def quarantined(example)
|
87
|
+
return "false" unless example.metadata.key?(:quarantine)
|
88
|
+
|
89
|
+
# if quarantine key is present and status is pending, consider it quarantined
|
90
|
+
(example.execution_result.status == :pending).to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
# Base ci job name
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
def job_name
|
97
|
+
@job_name ||= Runtime::Env.ci_job_name&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check if it is a merge request execution
|
101
|
+
#
|
102
|
+
# @return [String]
|
103
|
+
def merge_request
|
104
|
+
(!!merge_request_iid).to_s
|
105
|
+
end
|
106
|
+
|
107
|
+
# Merge request iid
|
108
|
+
#
|
109
|
+
# @return [String]
|
110
|
+
def merge_request_iid
|
111
|
+
env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID')
|
112
|
+
end
|
113
|
+
|
114
|
+
# Custom test metrics
|
115
|
+
#
|
116
|
+
# @param [Hash] metadata
|
117
|
+
# @param [Array] array of custom metrics keys
|
118
|
+
# @return [Hash]
|
119
|
+
def custom_metrics(metadata, custom_keys)
|
120
|
+
return {} if custom_keys.nil?
|
121
|
+
|
122
|
+
custom_metrics = {}
|
123
|
+
custom_keys.each do |k|
|
124
|
+
value = metadata[k.to_sym]
|
125
|
+
v = value.is_a?(Numeric) || value.nil? ? value : value.to_s
|
126
|
+
|
127
|
+
custom_metrics[k.to_sym] = v
|
128
|
+
end
|
129
|
+
|
130
|
+
custom_metrics
|
131
|
+
end
|
132
|
+
|
133
|
+
# Return non empty environment variable value
|
134
|
+
#
|
135
|
+
# @param [String] name
|
136
|
+
# @return [String, nil]
|
137
|
+
def env(name)
|
138
|
+
return unless ENV[name] && !ENV[name].empty?
|
139
|
+
|
140
|
+
ENV.fetch(name)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -12,11 +12,57 @@ module GitlabQuality
|
|
12
12
|
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>\S+)\)/
|
13
13
|
LATEST_REPORTS_TO_SHOW = 10
|
14
14
|
|
15
|
+
class ReportsList
|
16
|
+
def initialize(preserved_content:, section_header:, reports:, extra_content:)
|
17
|
+
@preserved_content = preserved_content
|
18
|
+
@section_header = section_header
|
19
|
+
@reports = reports
|
20
|
+
@extra_content = extra_content
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.report_list_item(test, item_extra_content: nil)
|
24
|
+
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')}) #{item_extra_content}".strip
|
25
|
+
end
|
26
|
+
|
27
|
+
def reports_count
|
28
|
+
reports.size
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
[
|
33
|
+
preserved_content,
|
34
|
+
"#{section_header} (#{reports_count})",
|
35
|
+
reports_list(reports),
|
36
|
+
extra_content
|
37
|
+
].reject(&:blank?).compact.join("\n\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :preserved_content, :section_header, :reports, :extra_content
|
43
|
+
|
44
|
+
def reports_list(reports)
|
45
|
+
sorted_reports = reports.sort.reverse
|
46
|
+
|
47
|
+
if sorted_reports.size > LATEST_REPORTS_TO_SHOW
|
48
|
+
[
|
49
|
+
"Last 10 reports:",
|
50
|
+
sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
|
51
|
+
"<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
|
52
|
+
sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
|
53
|
+
"</details>"
|
54
|
+
].join("\n\n")
|
55
|
+
else
|
56
|
+
sorted_reports.join("\n")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
15
61
|
def initial_reports_section(test)
|
16
62
|
<<~REPORTS
|
17
63
|
### Reports (1)
|
18
64
|
|
19
|
-
#{report_list_item(test)}
|
65
|
+
#{ReportsList.report_list_item(test)}
|
20
66
|
REPORTS
|
21
67
|
end
|
22
68
|
|
@@ -27,14 +73,14 @@ module GitlabQuality
|
|
27
73
|
item_extra_content: nil,
|
28
74
|
reports_extra_content: nil)
|
29
75
|
preserved_content = current_reports_content.split(reports_section_header).first&.strip
|
30
|
-
reports = report_lines(current_reports_content) + [report_list_item(test, item_extra_content: item_extra_content)]
|
31
|
-
|
32
|
-
|
33
|
-
preserved_content,
|
34
|
-
|
35
|
-
|
36
|
-
reports_extra_content
|
37
|
-
|
76
|
+
reports = report_lines(current_reports_content) + [ReportsList.report_list_item(test, item_extra_content: item_extra_content)]
|
77
|
+
|
78
|
+
ReportsList.new(
|
79
|
+
preserved_content: preserved_content,
|
80
|
+
section_header: reports_section_header,
|
81
|
+
reports: reports,
|
82
|
+
extra_content: reports_extra_content
|
83
|
+
)
|
38
84
|
end
|
39
85
|
|
40
86
|
def failed_issue_job_url(issue)
|
@@ -55,40 +101,12 @@ module GitlabQuality
|
|
55
101
|
content.lines.grep(REPORT_ITEM_REGEX).map(&:strip)
|
56
102
|
end
|
57
103
|
|
58
|
-
def reports_list(reports)
|
59
|
-
sorted_reports = reports.sort.reverse
|
60
|
-
|
61
|
-
if sorted_reports.size > LATEST_REPORTS_TO_SHOW
|
62
|
-
[
|
63
|
-
"Last 10 reports:",
|
64
|
-
sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
|
65
|
-
"<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
|
66
|
-
sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
|
67
|
-
"</details>"
|
68
|
-
].join("\n\n")
|
69
|
-
else
|
70
|
-
sorted_reports.join("\n")
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def report_list_item(test, item_extra_content: nil)
|
75
|
-
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')}) #{item_extra_content}".strip
|
76
|
-
end
|
77
|
-
|
78
104
|
def job_urls_from_description(issue_description, regex)
|
79
105
|
issue_description.lines.filter_map do |line|
|
80
106
|
match = line.match(regex)
|
81
107
|
match[:job_url] if match
|
82
108
|
end
|
83
109
|
end
|
84
|
-
|
85
|
-
def test_captures_to_report_items(test_captures)
|
86
|
-
test_captures.map do |ci_job_url, _, _|
|
87
|
-
report_list_item(GitlabQuality::TestTooling::TestResult::JsonTestResult.new(
|
88
|
-
'ci_job_url' => ci_job_url
|
89
|
-
))
|
90
|
-
end
|
91
|
-
end
|
92
110
|
end
|
93
111
|
end
|
94
112
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'amatch'
|
4
|
-
|
5
3
|
module GitlabQuality
|
6
4
|
module TestTooling
|
7
5
|
module Report
|
@@ -15,7 +13,6 @@ module GitlabQuality
|
|
15
13
|
class FailedTestIssue < ReportAsIssue
|
16
14
|
include Concerns::GroupAndCategoryLabels
|
17
15
|
include Concerns::IssueReports
|
18
|
-
include Amatch
|
19
16
|
|
20
17
|
IDENTITY_LABELS = ['test', 'automation:bot-authored'].freeze
|
21
18
|
NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'failure::new', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
|
@@ -25,10 +22,6 @@ module GitlabQuality
|
|
25
22
|
REPORTS_DISCUSSION_HEADER = '### Failure reports'
|
26
23
|
REPORT_SECTION_HEADER = '#### Failure reports'
|
27
24
|
|
28
|
-
IGNORED_FAILURES = [
|
29
|
-
'Net::ReadTimeout',
|
30
|
-
'403 Forbidden - Your account has been blocked'
|
31
|
-
].freeze
|
32
25
|
FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
|
33
26
|
ISSUE_STACKTRACE_REGEX = /##### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*/m
|
34
27
|
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
@@ -92,20 +85,21 @@ module GitlabQuality
|
|
92
85
|
end
|
93
86
|
end
|
94
87
|
|
95
|
-
def add_report_to_issue(issue:, test:, related_issues:)
|
88
|
+
def add_report_to_issue(issue:, test:, related_issues:) # rubocop:disable Metrics/AbcSize:
|
96
89
|
reports_discussion = find_or_create_reports_discussion(issue: issue)
|
97
|
-
|
90
|
+
current_reports_note = find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
|
91
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
98
92
|
|
99
93
|
note_body = [
|
100
|
-
|
94
|
+
new_reports_list.to_s,
|
101
95
|
identity_labels_quick_action,
|
102
96
|
relate_issues_quick_actions(related_issues)
|
103
97
|
].join("\n")
|
104
98
|
|
105
|
-
if
|
99
|
+
if current_reports_note
|
106
100
|
gitlab.edit_issue_note(
|
107
101
|
issue_iid: issue.iid,
|
108
|
-
note_id:
|
102
|
+
note_id: current_reports_note.id,
|
109
103
|
note: note_body
|
110
104
|
)
|
111
105
|
else
|
@@ -158,6 +152,8 @@ module GitlabQuality
|
|
158
152
|
|
159
153
|
clean_test_stacktrace = cleaned_stack_trace_from_test(test: test)
|
160
154
|
|
155
|
+
# We're skipping the first note of the discussion as this is the "non-collapsible note", aka
|
156
|
+
# the "header note", which doesn't contain any stack trace.
|
161
157
|
reports_discussion.notes[1..].each_with_object({}) do |note, memo|
|
162
158
|
clean_note_stacktrace = cleaned_stack_trace_from_note(issue: issue, note: note)
|
163
159
|
diff_ratio = diff_ratio_between_test_and_note_stacktraces(
|
@@ -171,7 +167,7 @@ module GitlabQuality
|
|
171
167
|
end
|
172
168
|
|
173
169
|
def cleaned_stack_trace_from_test(test:)
|
174
|
-
sanitize_stacktrace(stacktrace: full_stacktrace
|
170
|
+
sanitize_stacktrace(stacktrace: test.full_stacktrace, regex: FAILURE_STACKTRACE_REGEX) || test.full_stacktrace
|
175
171
|
end
|
176
172
|
|
177
173
|
def cleaned_stack_trace_from_note(issue:, note:)
|
@@ -191,53 +187,33 @@ module GitlabQuality
|
|
191
187
|
end
|
192
188
|
end
|
193
189
|
|
194
|
-
def full_stacktrace(test:)
|
195
|
-
test.failures.each do |failure|
|
196
|
-
message = failure['message'] || ""
|
197
|
-
message_lines = failure['message_lines'] || []
|
198
|
-
|
199
|
-
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
200
|
-
|
201
|
-
return message_lines.empty? ? message : message_lines.join("\n")
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
190
|
def diff_ratio_between_test_and_note_stacktraces(issue:, note:, test_stacktrace:, note_stacktrace:)
|
206
191
|
return if note_stacktrace.nil?
|
207
192
|
|
208
|
-
|
193
|
+
stack_trace_comparator = StackTraceComparator.new(test_stacktrace, note_stacktrace)
|
209
194
|
|
210
|
-
if
|
211
|
-
puts " => [DEBUG] Note #{issue.web_url}#note_#{note.id} has an acceptable diff ratio of #{
|
195
|
+
if stack_trace_comparator.lower_or_equal_to_diff_ratio?(max_diff_ratio)
|
196
|
+
puts " => [DEBUG] Note #{issue.web_url}#note_#{note.id} has an acceptable diff ratio of #{stack_trace_comparator.diff_percent}%."
|
212
197
|
# The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
|
213
198
|
# This leads to a `TypeError Exception: no implicit conversion of Hash into Integer` error, so we convert the object to a hash before using it as a Hash key.
|
214
199
|
# See:
|
215
200
|
# - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995
|
216
201
|
# - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
|
217
|
-
diff_ratio
|
202
|
+
stack_trace_comparator.diff_ratio
|
218
203
|
else
|
219
|
-
puts " => [DEBUG] Found note #{issue.web_url}#note_#{note.id} but stacktraces are too different (#{
|
204
|
+
puts " => [DEBUG] Found note #{issue.web_url}#note_#{note.id} but stacktraces are too different (#{stack_trace_comparator.diff_percent}%).\n"
|
220
205
|
puts " => [DEBUG] Issue stacktrace:\n----------------\n#{note_stacktrace}\n----------------\n"
|
221
206
|
puts " => [DEBUG] Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
|
222
207
|
end
|
223
208
|
end
|
224
209
|
|
225
|
-
def
|
226
|
-
calculate_diff_ratio(stack_trace_first, stack_trace_second)
|
227
|
-
end
|
228
|
-
|
229
|
-
def calculate_diff_ratio(stack_trace_first, stack_trace_second)
|
230
|
-
distance = Levenshtein.new(stack_trace_first).match(stack_trace_second)
|
231
|
-
distance.zero? ? 0.0 : (distance.to_f / stack_trace_first.size).round(3)
|
232
|
-
end
|
233
|
-
|
234
|
-
def report_body(reports_note:, test:)
|
210
|
+
def add_report_for_test(current_reports_content:, test:)
|
235
211
|
increment_reports(
|
236
|
-
current_reports_content:
|
212
|
+
current_reports_content: current_reports_content,
|
237
213
|
test: test,
|
238
214
|
reports_section_header: REPORT_SECTION_HEADER,
|
239
215
|
item_extra_content: found_label,
|
240
|
-
reports_extra_content: "##### Stack trace\n\n```\n#{full_stacktrace
|
216
|
+
reports_extra_content: "##### Stack trace\n\n```\n#{test.full_stacktrace}\n```"
|
241
217
|
)
|
242
218
|
end
|
243
219
|
|
@@ -22,7 +22,7 @@ module GitlabQuality
|
|
22
22
|
FOUND_IN_MASTER_LABEL = '~"found:master"'
|
23
23
|
REPORT_SECTION_HEADER = '### Flakiness reports'
|
24
24
|
REPORTS_DOCUMENTATION = <<~DOC
|
25
|
-
Flaky tests were detected
|
25
|
+
Flaky tests were detected. Please refer to the [Flaky tests reproducibility instructions](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html#how-to-reproduce-a-flaky-test-locally)
|
26
26
|
to learn more about how to reproduce them.
|
27
27
|
DOC
|
28
28
|
|
@@ -83,17 +83,20 @@ module GitlabQuality
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def add_report_to_issue(issue:, test:, related_issues:)
|
86
|
-
|
86
|
+
current_reports_note = existing_reports_note(issue: issue)
|
87
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
88
|
+
|
87
89
|
note_body = [
|
88
|
-
|
90
|
+
new_reports_list.to_s,
|
91
|
+
flakiness_status_labels_quick_action(new_reports_list.reports_count),
|
89
92
|
identity_labels_quick_action,
|
90
93
|
relate_issues_quick_actions(related_issues)
|
91
94
|
].join("\n")
|
92
95
|
|
93
|
-
if
|
96
|
+
if current_reports_note
|
94
97
|
gitlab.edit_issue_note(
|
95
98
|
issue_iid: issue.iid,
|
96
|
-
note_id:
|
99
|
+
note_id: current_reports_note.id,
|
97
100
|
note: note_body
|
98
101
|
)
|
99
102
|
else
|
@@ -107,9 +110,9 @@ module GitlabQuality
|
|
107
110
|
end
|
108
111
|
end
|
109
112
|
|
110
|
-
def
|
113
|
+
def add_report_for_test(current_reports_content:, test:)
|
111
114
|
increment_reports(
|
112
|
-
current_reports_content:
|
115
|
+
current_reports_content: current_reports_content,
|
113
116
|
test: test,
|
114
117
|
reports_section_header: REPORT_SECTION_HEADER,
|
115
118
|
item_extra_content: found_label,
|
@@ -125,6 +128,28 @@ module GitlabQuality
|
|
125
128
|
end
|
126
129
|
end
|
127
130
|
|
131
|
+
# The report count is based on the percentiles of flakiness issues.
|
132
|
+
#
|
133
|
+
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb
|
134
|
+
# to gather these statistics.
|
135
|
+
#
|
136
|
+
# P75 => flakiness::4
|
137
|
+
# P90 => flakiness::3
|
138
|
+
# P95 => flakiness::2
|
139
|
+
# Above P95 => flakiness::1
|
140
|
+
def flakiness_status_labels_quick_action(reports_count)
|
141
|
+
case reports_count
|
142
|
+
when 42..Float::INFINITY
|
143
|
+
'/label ~"flakiness::1"'
|
144
|
+
when 22..41
|
145
|
+
'/label ~"flakiness::2"'
|
146
|
+
when 4..21
|
147
|
+
'/label ~"flakiness::3"'
|
148
|
+
else
|
149
|
+
'/label ~"flakiness::4"'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
128
153
|
def identity_labels_quick_action
|
129
154
|
labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
|
130
155
|
%(/label #{labels_list})
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'nokogiri'
|
4
4
|
require 'rubygems/text'
|
5
|
-
require 'amatch'
|
6
5
|
|
7
6
|
module GitlabQuality
|
8
7
|
module TestTooling
|
@@ -17,7 +16,6 @@ module GitlabQuality
|
|
17
16
|
include TestTooling::Concerns::FindSetDri
|
18
17
|
include Concerns::GroupAndCategoryLabels
|
19
18
|
include Concerns::IssueReports
|
20
|
-
include Amatch
|
21
19
|
|
22
20
|
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
23
21
|
SYSTEMIC_EXCEPTIONS_THRESHOLD = 10
|
@@ -26,10 +24,6 @@ module GitlabQuality
|
|
26
24
|
ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*\n###/m
|
27
25
|
|
28
26
|
NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
|
29
|
-
IGNORED_FAILURES = [
|
30
|
-
'Net::ReadTimeout',
|
31
|
-
'403 Forbidden - Your account has been blocked'
|
32
|
-
].freeze
|
33
27
|
SCREENSHOT_IGNORED_ERRORS = ['500 Internal Server Error', 'fabricate_via_api!', 'Error Code 500'].freeze
|
34
28
|
|
35
29
|
MultipleIssuesFound = Class.new(StandardError)
|
@@ -168,7 +162,9 @@ module GitlabQuality
|
|
168
162
|
|
169
163
|
next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
|
170
164
|
|
171
|
-
|
165
|
+
stack_trace_comparator = StackTraceComparator.new(cleaned_stack_trace_from_test(test), cleaned_stack_trace_from_issue(issue))
|
166
|
+
|
167
|
+
stack_trace_comparator.lower_than_diff_ratio?(max_diff_ratio)
|
172
168
|
end
|
173
169
|
end
|
174
170
|
|
@@ -196,17 +192,6 @@ module GitlabQuality
|
|
196
192
|
)
|
197
193
|
end
|
198
194
|
|
199
|
-
def full_stacktrace(test)
|
200
|
-
test.failures.each do |failure|
|
201
|
-
message = failure['message'] || ""
|
202
|
-
message_lines = failure['message_lines'] || []
|
203
|
-
|
204
|
-
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
205
|
-
|
206
|
-
return message_lines.empty? ? message : message_lines.join("\n")
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
195
|
def cleaned_stack_trace_from_issue(issue)
|
211
196
|
relevant_issue_stacktrace = find_issue_stacktrace(issue)
|
212
197
|
return unless relevant_issue_stacktrace
|
@@ -215,20 +200,11 @@ module GitlabQuality
|
|
215
200
|
end
|
216
201
|
|
217
202
|
def cleaned_stack_trace_from_test(test)
|
218
|
-
test_failure_stacktrace = sanitize_stacktrace(full_stacktrace
|
219
|
-
FAILURE_STACKTRACE_REGEX) || full_stacktrace
|
203
|
+
test_failure_stacktrace = sanitize_stacktrace(test.full_stacktrace,
|
204
|
+
FAILURE_STACKTRACE_REGEX) || test.full_stacktrace
|
220
205
|
remove_unique_resource_names(test_failure_stacktrace)
|
221
206
|
end
|
222
207
|
|
223
|
-
def compare_stack_traces(stack_trace_first, stack_trace_second)
|
224
|
-
calculate_diff_ratio(stack_trace_first, stack_trace_second)
|
225
|
-
end
|
226
|
-
|
227
|
-
def calculate_diff_ratio(stack_trace_first, stack_trace_second)
|
228
|
-
distance = Levenshtein.new(stack_trace_first).match(stack_trace_second)
|
229
|
-
distance.zero? ? 0.0 : (distance.to_f / stack_trace_first.size).round(3)
|
230
|
-
end
|
231
|
-
|
232
208
|
def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
|
233
209
|
clean_first_test_failure_stacktrace = cleaned_stack_trace_from_test(test)
|
234
210
|
# Search with the `search` param returns 500 errors, so we filter by `base_issue_labels` and then filter further in Ruby
|
@@ -236,17 +212,18 @@ module GitlabQuality
|
|
236
212
|
clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
|
237
213
|
next if clean_relevant_issue_stacktrace.nil?
|
238
214
|
|
239
|
-
|
240
|
-
|
241
|
-
|
215
|
+
stack_trace_comparator = StackTraceComparator.new(clean_first_test_failure_stacktrace, clean_relevant_issue_stacktrace)
|
216
|
+
|
217
|
+
if stack_trace_comparator.lower_or_equal_to_diff_ratio?(max_diff_ratio)
|
218
|
+
puts " => [DEBUG] Issue #{issue.web_url} has an acceptable diff ratio of #{stack_trace_comparator.diff_percent}%."
|
242
219
|
# The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
|
243
220
|
# This leads to a `TypeError Exception: no implicit conversion of Hash into Integer` error, so we convert the object to a hash before using it as a Hash key.
|
244
221
|
# See:
|
245
222
|
# - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995
|
246
223
|
# - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
|
247
|
-
memo[issue.to_h] = diff_ratio
|
224
|
+
memo[issue.to_h] = stack_trace_comparator.diff_ratio
|
248
225
|
else
|
249
|
-
puts " => [DEBUG] Found issue #{issue.web_url} but stacktraces are too different (#{
|
226
|
+
puts " => [DEBUG] Found issue #{issue.web_url} but stacktraces are too different (#{stack_trace_comparator.diff_percent}%).\n"
|
250
227
|
puts " => [DEBUG] Issue stacktrace:\n----------------\n#{clean_relevant_issue_stacktrace}\n----------------\n"
|
251
228
|
puts " => [DEBUG] Failure stacktrace:\n----------------\n#{clean_first_test_failure_stacktrace}\n----------------\n"
|
252
229
|
end
|
@@ -295,7 +272,7 @@ module GitlabQuality
|
|
295
272
|
def new_issue_description(test)
|
296
273
|
super + [
|
297
274
|
"\n### Stack trace",
|
298
|
-
"```\n#{full_stacktrace
|
275
|
+
"```\n#{test.full_stacktrace}\n```",
|
299
276
|
screenshot_section(test),
|
300
277
|
system_log_errors_section(test),
|
301
278
|
initial_reports_section(test)
|
@@ -345,7 +322,7 @@ module GitlabQuality
|
|
345
322
|
state_event = issue.state == 'closed' ? 'reopen' : nil
|
346
323
|
|
347
324
|
issue_attrs = {
|
348
|
-
description: increment_reports(current_reports_content: issue.description, test: test),
|
325
|
+
description: increment_reports(current_reports_content: issue.description, test: test).to_s,
|
349
326
|
labels: up_to_date_labels(test: test, issue: issue)
|
350
327
|
}
|
351
328
|
issue_attrs[:state_event] = state_event if state_event
|
@@ -357,7 +334,7 @@ module GitlabQuality
|
|
357
334
|
def screenshot_section(test)
|
358
335
|
return unless test.screenshot?
|
359
336
|
|
360
|
-
failure = full_stacktrace
|
337
|
+
failure = test.full_stacktrace
|
361
338
|
return if SCREENSHOT_IGNORED_ERRORS.any? { |e| failure.include?(e) }
|
362
339
|
|
363
340
|
relative_url = gitlab.upload_file(file_fullpath: test.screenshot_image)
|
@@ -374,7 +351,7 @@ module GitlabQuality
|
|
374
351
|
return false unless test.failures?
|
375
352
|
|
376
353
|
puts " => Systemic failures detected: #{systemic_failure_messages}" if systemic_failure_messages.any?
|
377
|
-
failure_to_ignore = IGNORED_FAILURES + systemic_failure_messages
|
354
|
+
failure_to_ignore = TestResult::BaseTestResult::IGNORED_FAILURES + systemic_failure_messages
|
378
355
|
|
379
356
|
reason = ignored_failure_reason(test.failures, failure_to_ignore)
|
380
357
|
|
@@ -64,17 +64,20 @@ module GitlabQuality
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def add_report_to_issue(issue:, test:, related_issues:)
|
67
|
-
|
67
|
+
current_reports_note = existing_reports_note(issue: issue)
|
68
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
69
|
+
|
68
70
|
note_body = [
|
69
|
-
|
71
|
+
new_reports_list.to_s,
|
72
|
+
slowness_status_labels_quick_action(new_reports_list.reports_count),
|
70
73
|
identity_labels_quick_action,
|
71
74
|
relate_issues_quick_actions(related_issues)
|
72
75
|
].join("\n")
|
73
76
|
|
74
|
-
if
|
77
|
+
if current_reports_note
|
75
78
|
gitlab.edit_issue_note(
|
76
79
|
issue_iid: issue.iid,
|
77
|
-
note_id:
|
80
|
+
note_id: current_reports_note.id,
|
78
81
|
note: note_body
|
79
82
|
)
|
80
83
|
else
|
@@ -88,9 +91,9 @@ module GitlabQuality
|
|
88
91
|
end
|
89
92
|
end
|
90
93
|
|
91
|
-
def
|
94
|
+
def add_report_for_test(current_reports_content:, test:)
|
92
95
|
increment_reports(
|
93
|
-
current_reports_content:
|
96
|
+
current_reports_content: current_reports_content,
|
94
97
|
test: test,
|
95
98
|
reports_section_header: REPORT_SECTION_HEADER,
|
96
99
|
item_extra_content: "(#{test.run_time} seconds) #{found_label}",
|
@@ -106,6 +109,28 @@ module GitlabQuality
|
|
106
109
|
end
|
107
110
|
end
|
108
111
|
|
112
|
+
# The report count is based on the percentiles of slowness issues.
|
113
|
+
#
|
114
|
+
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb
|
115
|
+
# to gather these statistics.
|
116
|
+
#
|
117
|
+
# P75 => slowness::4
|
118
|
+
# P90 => slowness::3
|
119
|
+
# P95 => slowness::2
|
120
|
+
# Above P95 => slowness::1
|
121
|
+
def slowness_status_labels_quick_action(reports_count)
|
122
|
+
case reports_count
|
123
|
+
when 40..Float::INFINITY
|
124
|
+
'/label ~"slowness::1"'
|
125
|
+
when 28..39
|
126
|
+
'/label ~"slowness::2"'
|
127
|
+
when 12..27
|
128
|
+
'/label ~"slowness::3"'
|
129
|
+
else
|
130
|
+
'/label ~"slowness::4"'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
109
134
|
def identity_labels_quick_action
|
110
135
|
labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
|
111
136
|
%(/label #{labels_list})
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'amatch'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
class StackTraceComparator
|
8
|
+
include Amatch
|
9
|
+
|
10
|
+
def initialize(first_trace, second_trace)
|
11
|
+
@first_trace = first_trace
|
12
|
+
@second_trace = second_trace
|
13
|
+
end
|
14
|
+
|
15
|
+
def diff_ratio
|
16
|
+
@diff_ratio ||= (1 - first_trace.levenshtein_similar(second_trace))
|
17
|
+
end
|
18
|
+
|
19
|
+
def diff_percent
|
20
|
+
(diff_ratio * 100).round(2)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lower_than_diff_ratio?(max_diff_ratio)
|
24
|
+
diff_ratio < max_diff_ratio
|
25
|
+
end
|
26
|
+
|
27
|
+
def lower_or_equal_to_diff_ratio?(max_diff_ratio)
|
28
|
+
diff_ratio <= max_diff_ratio
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :first_trace, :second_trace
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
module TestMetric
|
8
|
+
class LogTestMetrics
|
9
|
+
include Concerns::TestMetrics
|
10
|
+
include Concerns::InfluxdbTools
|
11
|
+
|
12
|
+
CUSTOM_METRICS_KEY = :custom_test_metrics
|
13
|
+
|
14
|
+
def initialize(examples:, influxdb_url: nil, influxdb_token: nil, influxdb_bucket: nil, run_type: nil)
|
15
|
+
@examples = examples
|
16
|
+
@influxdb_url = influxdb_url
|
17
|
+
@influxdb_token = influxdb_token
|
18
|
+
@influxdb_bucket = influxdb_bucket
|
19
|
+
@run_type = run_type
|
20
|
+
end
|
21
|
+
|
22
|
+
# Push test execution metrics to influxdb
|
23
|
+
#
|
24
|
+
# @param [Array<String>] custom_keys_tags
|
25
|
+
# @param [Array<String>] custom_keys_fields
|
26
|
+
# @return [nil]
|
27
|
+
def push_test_metrics(custom_keys_tags: nil, custom_keys_fields: nil)
|
28
|
+
@test_metrics ||= examples.filter_map { |example| parse_test_results(example, custom_keys_tags, custom_keys_fields) }
|
29
|
+
|
30
|
+
write_api(url: influxdb_url, token: influxdb_token, bucket: influxdb_bucket).write(data: test_metrics)
|
31
|
+
Runtime::Logger.debug("Pushed #{test_metrics.length} test execution entries to influxdb")
|
32
|
+
rescue StandardError => e
|
33
|
+
Runtime::Logger.error("Failed to push test execution metrics to influxdb, error: #{e}")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Save metrics in json file
|
37
|
+
#
|
38
|
+
# @param [String] file_name
|
39
|
+
# @param [Array<String>] custom_keys_tags
|
40
|
+
# @param [Array<String>] custom_keys_fields
|
41
|
+
# @return [nil]
|
42
|
+
def save_test_metrics(file_name, custom_keys_tags: nil, custom_keys_fields: nil)
|
43
|
+
@test_metrics ||= examples.filter_map { |example| parse_test_results(example, custom_keys_tags, custom_keys_fields) }
|
44
|
+
file = "tmp/#{file_name}"
|
45
|
+
|
46
|
+
File.write(file, test_metrics.to_json) && Runtime::Logger.debug("Saved test metrics to #{file}")
|
47
|
+
rescue StandardError => e
|
48
|
+
Runtime::Logger.error("Failed to save test execution metrics, error: #{e}")
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_reader :examples, :test_metrics, :influxdb_url, :influxdb_token, :influxdb_bucket, :run_type
|
54
|
+
|
55
|
+
# Transform example to influxdb compatible metrics data
|
56
|
+
# https://github.com/influxdata/influxdb-client-ruby#data-format
|
57
|
+
#
|
58
|
+
# @param [RSpec::Core::Example] example
|
59
|
+
# @param [Array<String>] custom_keys_tags
|
60
|
+
# @param [Array<String>] custom_keys_fields
|
61
|
+
# @return [Hash]
|
62
|
+
def parse_test_results(example, custom_keys_tags, custom_keys_fields)
|
63
|
+
{
|
64
|
+
name: 'test-stats',
|
65
|
+
time: time,
|
66
|
+
tags: tags(example, custom_keys_tags, run_type),
|
67
|
+
fields: fields(example, custom_keys_fields)
|
68
|
+
}
|
69
|
+
rescue StandardError => e
|
70
|
+
Runtime::Logger.error("Failed to transform example '#{example.id}', error: #{e}")
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -4,6 +4,11 @@ module GitlabQuality
|
|
4
4
|
module TestTooling
|
5
5
|
module TestResult
|
6
6
|
class BaseTestResult
|
7
|
+
IGNORED_FAILURES = [
|
8
|
+
'Net::ReadTimeout',
|
9
|
+
'403 Forbidden - Your account has been blocked'
|
10
|
+
].freeze
|
11
|
+
|
7
12
|
attr_reader :report
|
8
13
|
|
9
14
|
def initialize(report)
|
@@ -41,6 +46,17 @@ module GitlabQuality
|
|
41
46
|
def failures?
|
42
47
|
failures.any?
|
43
48
|
end
|
49
|
+
|
50
|
+
def full_stacktrace
|
51
|
+
failures.each do |failure|
|
52
|
+
message = failure['message'] || ""
|
53
|
+
message_lines = failure['message_lines'] || []
|
54
|
+
|
55
|
+
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
56
|
+
|
57
|
+
return message_lines.empty? ? message : message_lines.join("\n")
|
58
|
+
end
|
59
|
+
end
|
44
60
|
end
|
45
61
|
end
|
46
62
|
end
|
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab_quality-test_tooling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.24.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: 2024-04-
|
11
|
+
date: 2024-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7.2'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.1'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.2'
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: climate_control
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +86,20 @@ dependencies:
|
|
66
86
|
- - "~>"
|
67
87
|
- !ruby/object:Gem::Version
|
68
88
|
version: '4.7'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: influxdb-client
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.1'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.1'
|
69
103
|
- !ruby/object:Gem::Dependency
|
70
104
|
name: lefthook
|
71
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -404,6 +438,8 @@ files:
|
|
404
438
|
- lefthook.yml
|
405
439
|
- lib/gitlab_quality/test_tooling.rb
|
406
440
|
- lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb
|
441
|
+
- lib/gitlab_quality/test_tooling/concerns/influxdb_tools.rb
|
442
|
+
- lib/gitlab_quality/test_tooling/concerns/test_metrics.rb
|
407
443
|
- lib/gitlab_quality/test_tooling/failed_jobs_table.rb
|
408
444
|
- lib/gitlab_quality/test_tooling/gitlab_client/branches_client.rb
|
409
445
|
- lib/gitlab_quality/test_tooling/gitlab_client/branches_dry_client.rb
|
@@ -441,6 +477,7 @@ files:
|
|
441
477
|
- lib/gitlab_quality/test_tooling/runtime/logger.rb
|
442
478
|
- lib/gitlab_quality/test_tooling/slack/post_to_slack.rb
|
443
479
|
- lib/gitlab_quality/test_tooling/slack/post_to_slack_dry.rb
|
480
|
+
- lib/gitlab_quality/test_tooling/stack_trace_comparator.rb
|
444
481
|
- lib/gitlab_quality/test_tooling/summary_table.rb
|
445
482
|
- lib/gitlab_quality/test_tooling/support/http_request.rb
|
446
483
|
- lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb
|
@@ -460,6 +497,7 @@ files:
|
|
460
497
|
- lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb
|
461
498
|
- lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb
|
462
499
|
- lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
|
500
|
+
- lib/gitlab_quality/test_tooling/test_metric/log_test_metrics.rb
|
463
501
|
- lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
|
464
502
|
- lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
|
465
503
|
- lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb
|
@@ -492,7 +530,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
492
530
|
- !ruby/object:Gem::Version
|
493
531
|
version: '0'
|
494
532
|
requirements: []
|
495
|
-
rubygems_version: 3.3.
|
533
|
+
rubygems_version: 3.3.27
|
496
534
|
signing_key:
|
497
535
|
specification_version: 4
|
498
536
|
summary: A collection of test-related tools.
|