gitlab_quality-test_tooling 3.21.4 → 3.22.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/.rubocop_todo.yml +0 -1
- data/Gemfile.lock +1 -1
- data/README.md +0 -19
- data/exe/test-coverage +1 -2
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +115 -41
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +13 -10
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -8
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +2 -8
- data/exe/report-results +0 -62
- data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +0 -88
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +0 -77
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +0 -123
- data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +0 -96
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2bfb7b97e0b3a58ac7cbf7cc5f4ab8eb49bf3bf117fce216a34996ed4b4e471
|
|
4
|
+
data.tar.gz: 93b21c6cf0608d72292514aa34770db9f7402813ac6b0aeafc383b74f48202b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b58cd4d33e53fccf07cd9ca8a1113f0e93a50f33a2217f46c50c3d4ec3e5a1e2f777fbd5546ec20813385a913e989f29b56fbb936be7a5b0bff1fe4f2ffd9231
|
|
7
|
+
data.tar.gz: 06a62a7194ebbdc04483a4691eef617c3e2d82d553851f82430ddef4634c0a53f1cb4d3e8cda82fb648bc57ef6a2dda581e4c774cc3eacb622473d7c86d6466d
|
data/.rubocop_todo.yml
CHANGED
|
@@ -31,7 +31,6 @@ Layout/ClassStructure:
|
|
|
31
31
|
Layout/LineEndStringConcatenationIndentation:
|
|
32
32
|
Exclude:
|
|
33
33
|
- 'lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb'
|
|
34
|
-
- 'lib/gitlab_quality/test_tooling/report/report_results.rb'
|
|
35
34
|
- 'spec/gitlab_quality/test_tooling/gitlab_client/issues_client_spec.rb'
|
|
36
35
|
- 'spec/gitlab_quality/test_tooling/report/failed_test_issue_spec.rb'
|
|
37
36
|
- 'spec/gitlab_quality/test_tooling/report/slow_test_issue_spec.rb'
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -98,25 +98,6 @@ Usage: exe/relate-failure-issue [options]
|
|
|
98
98
|
-h, --help Show the usage
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
### `exe/report-results`
|
|
102
|
-
|
|
103
|
-
```shell
|
|
104
|
-
Purpose: Report test results from RSpec report files (JSON or JUnit XML) in GitLab test cases and result issues
|
|
105
|
-
Usage: exe/report-results [options]
|
|
106
|
-
-i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
|
|
107
|
-
--test-case-project TEST_CASE_PROJECT
|
|
108
|
-
Can be an integer or a group/project string
|
|
109
|
-
-t TEST_CASE_PROJECT_TOKEN, A valid access token with `api` scope and Reporter permission in TEST_CASE_PROJECT
|
|
110
|
-
--test-case-project-token
|
|
111
|
-
--results-issue-project RESULTS_ISSUE_PROJECT
|
|
112
|
-
Can be an integer or a group/project string
|
|
113
|
-
-r RESULTS_ISSUE_PROJECT_TOKEN, A valid access token with `api` scope and Reporter permission in RESULTS_ISSUE_PROJECT
|
|
114
|
-
--results-issue-project-token
|
|
115
|
-
--dry-run Perform a dry-run (don't create/update issues or test cases)
|
|
116
|
-
-v, --version Show the version
|
|
117
|
-
-h, --help Show the usage
|
|
118
|
-
```
|
|
119
|
-
|
|
120
101
|
### `exe/update-screenshot-paths`
|
|
121
102
|
|
|
122
103
|
```shell
|
data/exe/test-coverage
CHANGED
|
@@ -283,8 +283,7 @@ when :full_export
|
|
|
283
283
|
# Export test-to-file mappings
|
|
284
284
|
test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(
|
|
285
285
|
test_to_sources,
|
|
286
|
-
tests_to_categories: tests_to_categories
|
|
287
|
-
feature_categories_to_teams: category_owners.feature_categories_to_teams
|
|
286
|
+
tests_to_categories: tests_to_categories
|
|
288
287
|
)
|
|
289
288
|
test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
|
|
290
289
|
test_file_mappings_table.push(test_file_mapping_data.as_db_table)
|
|
@@ -9,8 +9,7 @@ module GitlabQuality
|
|
|
9
9
|
class TestFileMappingsTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
|
|
10
10
|
TABLE_NAME = "test_file_mappings"
|
|
11
11
|
|
|
12
|
-
# Override push to
|
|
13
|
-
# This prevents accumulating 36M duplicate rows per day in ReplacingMergeTree
|
|
12
|
+
# Override push to skip mappings already stored with the same values.
|
|
14
13
|
#
|
|
15
14
|
# @param data [Array<Hash>] Code coverage related data to be pushed to ClickHouse
|
|
16
15
|
# @return [nil]
|
|
@@ -18,7 +17,9 @@ module GitlabQuality
|
|
|
18
17
|
return logger.warn("#{LOG_PREFIX} No data found, skipping ClickHouse export!") if data.empty?
|
|
19
18
|
|
|
20
19
|
logger.debug("#{LOG_PREFIX} Starting data export to ClickHouse")
|
|
20
|
+
@dropped_source_files = []
|
|
21
21
|
sanitized_data = sanitize(data)
|
|
22
|
+
report_dropped_source_files(sanitized_data.size)
|
|
22
23
|
|
|
23
24
|
return logger.warn("#{LOG_PREFIX} No valid data found after sanitization, skipping ClickHouse export!") if sanitized_data.empty?
|
|
24
25
|
|
|
@@ -26,14 +27,14 @@ module GitlabQuality
|
|
|
26
27
|
new_or_changed_records = filter_duplicates(sanitized_data)
|
|
27
28
|
|
|
28
29
|
if new_or_changed_records.empty?
|
|
29
|
-
logger.info("#{LOG_PREFIX}
|
|
30
|
+
logger.info("#{LOG_PREFIX} None of the #{sanitized_data.size} mappings need writing to #{full_table_name}, skipping insert")
|
|
30
31
|
return
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
client.insert_json_data(table_name, new_or_changed_records)
|
|
34
35
|
skipped_count = sanitized_data.size - new_or_changed_records.size
|
|
35
36
|
logger.info("#{LOG_PREFIX} Successfully pushed #{new_or_changed_records.size} new/changed records " \
|
|
36
|
-
"to #{full_table_name} (
|
|
37
|
+
"to #{full_table_name} (#{skipped_count} not written)")
|
|
37
38
|
rescue StandardError => e
|
|
38
39
|
logger.error("#{LOG_PREFIX} Error occurred while pushing data to #{full_table_name}: #{e.message}")
|
|
39
40
|
raise
|
|
@@ -62,30 +63,74 @@ module GitlabQuality
|
|
|
62
63
|
false
|
|
63
64
|
end
|
|
64
65
|
|
|
65
|
-
# @return [Hash] Transformed mapping data including timestamp and CI metadata
|
|
66
|
+
# @return [Hash, nil] Transformed mapping data including timestamp and CI metadata,
|
|
67
|
+
# or nil when the source path cannot be made repo-relative
|
|
66
68
|
def sanitized_data_record(record)
|
|
69
|
+
source_file = relative_source_file(record[:source_file])
|
|
70
|
+
|
|
71
|
+
if source_file.nil?
|
|
72
|
+
dropped_source_files << record[:source_file]
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
67
76
|
{
|
|
68
77
|
timestamp: time,
|
|
69
78
|
test_file: record[:test_file],
|
|
70
|
-
source_file:
|
|
79
|
+
source_file: source_file,
|
|
71
80
|
# CI_PROJECT_PATH is set by GitLab CI runner and considered trusted infrastructure input.
|
|
72
81
|
# In non-CI environments, this may be user-controlled, but sanitize_for_clickhouse
|
|
73
82
|
# provides SQL injection protection.
|
|
74
83
|
ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil),
|
|
75
|
-
category: record[:category] || ''
|
|
76
|
-
group: record[:group] || '',
|
|
77
|
-
stage: record[:stage] || '',
|
|
78
|
-
section: record[:section] || ''
|
|
84
|
+
category: record[:category] || ''
|
|
79
85
|
}
|
|
80
86
|
end
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
def dropped_source_files
|
|
89
|
+
@dropped_source_files ||= []
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Reports the records that relative_source_file could not make repo-relative, once
|
|
93
|
+
# for the whole run, and raises when none survived. A CI_PROJECT_DIR that does not
|
|
94
|
+
# match the artifact's paths drops every mapping, so warning per record buries the
|
|
95
|
+
# job trace in one line per mapping, and warning alone would let a wholly empty
|
|
96
|
+
# export finish green.
|
|
97
|
+
#
|
|
98
|
+
# @return [void]
|
|
99
|
+
def report_dropped_source_files(kept_count)
|
|
100
|
+
return if dropped_source_files.empty?
|
|
101
|
+
|
|
102
|
+
message = "#{LOG_PREFIX} Skipped #{dropped_source_files.size} records with " \
|
|
103
|
+
"non-relative source_file, samples: #{dropped_source_files.first(3).join(', ')}"
|
|
104
|
+
|
|
105
|
+
raise message if kept_count.zero?
|
|
106
|
+
|
|
107
|
+
logger.warn(message)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The repo-relative form of a source path, which is what the table's
|
|
111
|
+
# valid_source_file_format constraint accepts. Returns nil when the path cannot be
|
|
112
|
+
# made relative or normalizes to nothing, and the caller drops that record, because
|
|
113
|
+
# ClickHouse rejects the whole insert batch on a constraint violation rather than
|
|
114
|
+
# the offending row.
|
|
115
|
+
#
|
|
116
|
+
# @return [String, nil]
|
|
117
|
+
def relative_source_file(path)
|
|
118
|
+
ci_project_dir = ENV.fetch('CI_PROJECT_DIR', nil)
|
|
119
|
+
|
|
120
|
+
relative = path
|
|
121
|
+
relative = relative.delete_prefix("#{ci_project_dir.chomp('/')}/") if ci_project_dir.present?
|
|
122
|
+
relative = relative.sub(%r{\A(\./)+}, '')
|
|
123
|
+
|
|
124
|
+
relative.presence unless relative.start_with?('/')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The records worth writing: mappings this project has not stored yet, plus ones
|
|
128
|
+
# whose category changed, except that a blank never replaces a stored category.
|
|
129
|
+
# Re-sending an unchanged mapping costs a row that survives until the table's TTL
|
|
130
|
+
# expires it, because section and stage are still part of the sort key and the
|
|
131
|
+
# values written here no longer match the stored ones.
|
|
86
132
|
#
|
|
87
|
-
#
|
|
88
|
-
# ownership (category/group/stage/section) changes over time.
|
|
133
|
+
# If the stored categories cannot be read, see #insertable_without_lookup.
|
|
89
134
|
#
|
|
90
135
|
# @param records [Array<Hash>] Sanitized records to insert
|
|
91
136
|
# @return [Array<Hash>] Records that are new or have changed values
|
|
@@ -98,11 +143,11 @@ module GitlabQuality
|
|
|
98
143
|
|
|
99
144
|
logger.debug("#{LOG_PREFIX} Checking for duplicate mappings in #{full_table_name}")
|
|
100
145
|
|
|
101
|
-
# Build a hash of existing mappings with their current enriched column values
|
|
102
|
-
# Key: "test_file|source_file", Value: {category, group, stage, section}
|
|
103
146
|
existing_mappings = fetch_existing_mappings(ci_project_path, records)
|
|
147
|
+
return insertable_without_lookup(records) if existing_mappings.nil?
|
|
148
|
+
|
|
149
|
+
blank_suppressed = 0
|
|
104
150
|
|
|
105
|
-
# Filter records: keep only new or changed mappings
|
|
106
151
|
new_or_changed = records.select do |record|
|
|
107
152
|
key = "#{record[:test_file]}|#{record[:source_file]}"
|
|
108
153
|
existing = existing_mappings[key]
|
|
@@ -110,27 +155,64 @@ module GitlabQuality
|
|
|
110
155
|
# New mapping - doesn't exist yet
|
|
111
156
|
next true if existing.nil?
|
|
112
157
|
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
158
|
+
# A blank category means this pipeline's rspec reports didn't cover the test,
|
|
159
|
+
# not that the test has no category: the mappings come from the Crystalball map,
|
|
160
|
+
# which lists every test file regardless of what ran. Writing the blank erases a
|
|
161
|
+
# known category, and readers resolving ownership through category get nothing.
|
|
162
|
+
if record[:category].blank? && existing[:category].present?
|
|
163
|
+
blank_suppressed += 1
|
|
164
|
+
next false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
existing[:category] != record[:category]
|
|
118
168
|
end
|
|
119
169
|
|
|
170
|
+
log_blank_suppressions(blank_suppressed)
|
|
120
171
|
logger.debug("#{LOG_PREFIX} Found #{new_or_changed.size} new/changed mappings out of #{records.size} total")
|
|
121
172
|
new_or_changed
|
|
122
173
|
rescue StandardError => e
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
logger.warn("#{LOG_PREFIX} Failed to check for duplicates: #{e.message}")
|
|
175
|
+
insertable_without_lookup(records)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# What to write when the stored categories could not be read. Records carrying a
|
|
179
|
+
# category still go in, because a failed read must not silently drop mappings.
|
|
180
|
+
# Blanks are held back: with no stored value to compare against they look new, and
|
|
181
|
+
# writing one replaces whatever category is already there. The mapping comes back
|
|
182
|
+
# on the next export that can read.
|
|
183
|
+
#
|
|
184
|
+
# @param records [Array<Hash>] Sanitized records to insert
|
|
185
|
+
# @return [Array<Hash>] Records that carry a category
|
|
186
|
+
def insertable_without_lookup(records)
|
|
187
|
+
with_category = records.reject { |record| record[:category].blank? }
|
|
188
|
+
|
|
189
|
+
logger.warn("#{LOG_PREFIX} No duplicate check possible; inserting " \
|
|
190
|
+
"#{with_category.size} of #{records.size} mappings and holding back " \
|
|
191
|
+
"#{records.size - with_category.size} with no category")
|
|
192
|
+
|
|
193
|
+
with_category
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Reported at info because it is the only signal that a pipeline's reports were
|
|
197
|
+
# incomplete. A steady non-zero count across runs means categories are arriving
|
|
198
|
+
# from fewer reports than there are test files in the map.
|
|
199
|
+
#
|
|
200
|
+
# @param count [Integer] Mappings whose stored category was kept over an incoming blank
|
|
201
|
+
# @return [nil]
|
|
202
|
+
def log_blank_suppressions(count)
|
|
203
|
+
return unless count.positive?
|
|
204
|
+
|
|
205
|
+
logger.info("#{LOG_PREFIX} Kept the stored category for #{count} mappings " \
|
|
206
|
+
"this pipeline's reports did not cover")
|
|
126
207
|
end
|
|
127
208
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
209
|
+
# The category currently stored against each of this project's mappings. Only
|
|
210
|
+
# filter_duplicates reads it.
|
|
130
211
|
#
|
|
131
212
|
# @param ci_project_path [String] The CI project path to filter by
|
|
132
213
|
# @param records [Array<Hash>] Records being inserted (used to limit query scope)
|
|
133
|
-
# @return [Hash]
|
|
214
|
+
# @return [Hash, nil] "test_file|source_file" => { category: String }, or nil when
|
|
215
|
+
# the read fails. An empty Hash means the table holds nothing for this project.
|
|
134
216
|
def fetch_existing_mappings(ci_project_path, records) # rubocop:disable Metrics/AbcSize
|
|
135
217
|
# Query for ALL latest mappings for this project
|
|
136
218
|
# We filter in Ruby rather than building dynamic SQL to avoid injection risks
|
|
@@ -142,10 +224,7 @@ module GitlabQuality
|
|
|
142
224
|
SELECT
|
|
143
225
|
test_file,
|
|
144
226
|
source_file,
|
|
145
|
-
argMax(category, timestamp) AS category
|
|
146
|
-
argMax(`group`, timestamp) AS `group`,
|
|
147
|
-
argMax(stage, timestamp) AS stage,
|
|
148
|
-
argMax(section, timestamp) AS section
|
|
227
|
+
argMax(category, timestamp) AS category
|
|
149
228
|
FROM #{full_table_name}
|
|
150
229
|
WHERE ci_project_path = {project_path:String}
|
|
151
230
|
GROUP BY test_file, source_file
|
|
@@ -163,16 +242,11 @@ module GitlabQuality
|
|
|
163
242
|
next if test_files_set && !test_files_set.include?(row['test_file'])
|
|
164
243
|
|
|
165
244
|
key = "#{row['test_file']}|#{row['source_file']}"
|
|
166
|
-
hash[key] = {
|
|
167
|
-
category: row['category'],
|
|
168
|
-
group: row['group'],
|
|
169
|
-
stage: row['stage'],
|
|
170
|
-
section: row['section']
|
|
171
|
-
}
|
|
245
|
+
hash[key] = { category: row['category'] }
|
|
172
246
|
end
|
|
173
247
|
rescue StandardError => e
|
|
174
248
|
logger.warn("#{LOG_PREFIX} Failed to fetch existing mappings: #{e.message}")
|
|
175
|
-
{}
|
|
249
|
+
nil # nil, not {}: an empty table and an unreadable one need different handling
|
|
176
250
|
end
|
|
177
251
|
|
|
178
252
|
# Sanitize string for ClickHouse by escaping single quotes and backslashes
|
|
@@ -8,34 +8,37 @@ module GitlabQuality
|
|
|
8
8
|
# mapped to all source files they cover
|
|
9
9
|
# @param [Hash<String, Array<String>>] tests_to_categories Test files
|
|
10
10
|
# mapped to their feature categories
|
|
11
|
-
|
|
12
|
-
# mapped to their org hierarchy (group, stage, section)
|
|
13
|
-
def initialize(test_to_sources, tests_to_categories: {}, feature_categories_to_teams: {})
|
|
11
|
+
def initialize(test_to_sources, tests_to_categories: {})
|
|
14
12
|
@test_to_sources = test_to_sources
|
|
15
13
|
@tests_to_categories = tests_to_categories
|
|
16
|
-
@feature_categories_to_teams = feature_categories_to_teams
|
|
17
14
|
end
|
|
18
15
|
|
|
16
|
+
# Rows for shared.test_file_mappings, one per test-and-source pair.
|
|
17
|
+
#
|
|
18
|
+
# Carries no group, stage or section on purpose. Those describe the category
|
|
19
|
+
# rather than the mapping and change whenever stages.yml is reorganised, so
|
|
20
|
+
# storing them on every mapping multiplies rows for no gain.
|
|
21
|
+
#
|
|
22
|
+
# Readers resolve ownership from shared.category_owners through category. That
|
|
23
|
+
# table keeps every historical row, up to 57 for one category, so a plain join
|
|
24
|
+
# fans out; dedupe it with ORDER BY category, timestamp DESC LIMIT 1 BY category.
|
|
25
|
+
#
|
|
19
26
|
# @return [Array<Hash<Symbol, String>>] Mapping data formatted for database insertion
|
|
20
27
|
# @example Return value
|
|
21
28
|
# [
|
|
22
29
|
# { test_file: "spec/models/user_spec.rb", source_file: "app/models/user.rb",
|
|
23
|
-
# category: "team_planning"
|
|
30
|
+
# category: "team_planning" },
|
|
24
31
|
# ...
|
|
25
32
|
# ]
|
|
26
33
|
def as_db_table
|
|
27
34
|
@test_to_sources.flat_map do |test_file, source_files|
|
|
28
35
|
category = @tests_to_categories[test_file]&.first || ''
|
|
29
|
-
team = @feature_categories_to_teams[category] || {}
|
|
30
36
|
|
|
31
37
|
source_files.map do |source_file|
|
|
32
38
|
{
|
|
33
39
|
test_file: test_file,
|
|
34
40
|
source_file: source_file,
|
|
35
|
-
category: category
|
|
36
|
-
group: team[:group] || '',
|
|
37
|
-
stage: team[:stage] || '',
|
|
38
|
-
section: team[:section] || ''
|
|
41
|
+
category: category
|
|
39
42
|
}
|
|
40
43
|
end
|
|
41
44
|
end
|
|
@@ -134,14 +134,6 @@ module GitlabQuality
|
|
|
134
134
|
issue&.labels&.to_set || Set.new
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
def update_labels(issue, test, new_labels = Set.new)
|
|
138
|
-
labels = up_to_date_labels(test: test, issue: issue, new_labels: new_labels)
|
|
139
|
-
|
|
140
|
-
return if issue_labels(issue) == labels
|
|
141
|
-
|
|
142
|
-
gitlab.edit_issue(iid: issue.iid, options: { labels: labels.to_a })
|
|
143
|
-
end
|
|
144
|
-
|
|
145
137
|
# Infer labels from the test, and optionally from the issue and new_labels in arguments
|
|
146
138
|
#
|
|
147
139
|
# Called when we're updating a test health issue with a new test report.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab_quality-test_tooling
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab Quality
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -437,7 +437,6 @@ executables:
|
|
|
437
437
|
- post-to-slack
|
|
438
438
|
- prepare-stage-reports
|
|
439
439
|
- relate-failure-issue
|
|
440
|
-
- report-results
|
|
441
440
|
- slow-test-issues
|
|
442
441
|
- slow-test-merge-request-report-note
|
|
443
442
|
- sync-category-owners
|
|
@@ -471,7 +470,6 @@ files:
|
|
|
471
470
|
- exe/post-to-slack
|
|
472
471
|
- exe/prepare-stage-reports
|
|
473
472
|
- exe/relate-failure-issue
|
|
474
|
-
- exe/report-results
|
|
475
473
|
- exe/slow-test-issues
|
|
476
474
|
- exe/slow-test-merge-request-report-note
|
|
477
475
|
- exe/sync-category-owners
|
|
@@ -539,7 +537,6 @@ files:
|
|
|
539
537
|
- lib/gitlab_quality/test_tooling/labels_inference.rb
|
|
540
538
|
- lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
|
|
541
539
|
- lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
|
|
542
|
-
- lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
|
|
543
540
|
- lib/gitlab_quality/test_tooling/report/concerns/utils.rb
|
|
544
541
|
- lib/gitlab_quality/test_tooling/report/failed_test_issue.rb
|
|
545
542
|
- lib/gitlab_quality/test_tooling/report/feature_readiness/report_on_epic.rb
|
|
@@ -561,9 +558,6 @@ files:
|
|
|
561
558
|
- lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb
|
|
562
559
|
- lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb
|
|
563
560
|
- lib/gitlab_quality/test_tooling/report/report_as_issue.rb
|
|
564
|
-
- lib/gitlab_quality/test_tooling/report/report_results.rb
|
|
565
|
-
- lib/gitlab_quality/test_tooling/report/results_in_issues.rb
|
|
566
|
-
- lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb
|
|
567
561
|
- lib/gitlab_quality/test_tooling/report/slow_test_issue.rb
|
|
568
562
|
- lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb
|
|
569
563
|
- lib/gitlab_quality/test_tooling/runtime/env.rb
|
data/exe/report-results
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "bundler/setup"
|
|
5
|
-
require "optparse"
|
|
6
|
-
|
|
7
|
-
require_relative "../lib/gitlab_quality/test_tooling"
|
|
8
|
-
|
|
9
|
-
params = {}
|
|
10
|
-
|
|
11
|
-
options = OptionParser.new do |opts|
|
|
12
|
-
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
13
|
-
|
|
14
|
-
opts.on('-i', '--input-files INPUT_FILES', String, 'RSpec report files (JSON or JUnit XML)') do |input_files|
|
|
15
|
-
params[:input_files] = input_files
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
opts.on('--test-case-project TEST_CASE_PROJECT', String,
|
|
19
|
-
'Can be an integer or a group/project string') do |test_case_project|
|
|
20
|
-
params[:test_case_project] = test_case_project
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
opts.on('-t', '--test-case-project-token TEST_CASE_PROJECT_TOKEN', String,
|
|
24
|
-
'A valid access token with `api` scope and Reporter permission in TEST_CASE_PROJECT') do |test_case_project_token|
|
|
25
|
-
params[:test_case_project_token] = test_case_project_token
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
opts.on('--results-issue-project RESULTS_ISSUE_PROJECT', String,
|
|
29
|
-
'Can be an integer or a group/project string') do |results_issue_project|
|
|
30
|
-
params[:results_issue_project] = results_issue_project
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
opts.on('-r', '--results-issue-project-token RESULTS_ISSUE_PROJECT_TOKEN', String,
|
|
34
|
-
'A valid access token with `api` scope and Reporter permission in RESULTS_ISSUE_PROJECT') do |results_issue_project_token|
|
|
35
|
-
params[:results_issue_project_token] = results_issue_project_token
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
opts.on('--dry-run', "Perform a dry-run (don't create/update issues or test cases)") do
|
|
39
|
-
params[:dry_run] = true
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
opts.on_tail('-v', '--version', 'Show the version') do
|
|
43
|
-
require_relative "../lib/gitlab_quality/test_tooling/version"
|
|
44
|
-
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
|
45
|
-
exit
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
opts.on_tail('-h', '--help', 'Show the usage') do
|
|
49
|
-
puts "Purpose: Report test results from RSpec report files (JSON or JUnit XML) in GitLab test cases and result issues"
|
|
50
|
-
puts opts
|
|
51
|
-
exit
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
opts.parse(ARGV)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
if params.any?
|
|
58
|
-
GitlabQuality::TestTooling::Report::ReportResults.new(**params).invoke!
|
|
59
|
-
else
|
|
60
|
-
puts options
|
|
61
|
-
exit 1
|
|
62
|
-
end
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'active_support/core_ext/enumerable'
|
|
4
|
-
|
|
5
|
-
module GitlabQuality
|
|
6
|
-
module TestTooling
|
|
7
|
-
module Report
|
|
8
|
-
module Concerns
|
|
9
|
-
module ResultsReporter
|
|
10
|
-
include Concerns::Utils
|
|
11
|
-
|
|
12
|
-
TEST_CASE_RESULTS_SECTION_TEMPLATE = "\n\n### DO NOT EDIT BELOW THIS LINE\n\nActive and historical test results:"
|
|
13
|
-
|
|
14
|
-
def find_issue(test)
|
|
15
|
-
issues = search_for_issues(test)
|
|
16
|
-
|
|
17
|
-
warn(%(Too many #{issue_type}s found with the file path "#{test.file}" and name "#{test.name}")) if issues.many?
|
|
18
|
-
|
|
19
|
-
puts "Found existing #{issue_type}: #{issues.first.web_url}" unless issues.empty?
|
|
20
|
-
|
|
21
|
-
issues.first
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def find_issue_by_iid(iid)
|
|
25
|
-
issues = gitlab.find_issues(iid: iid) do |issue|
|
|
26
|
-
issue.state == 'opened' && issue.issue_type == issue_type
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
warn(%(#{issue_type} iid "#{iid}" not valid)) if issues.empty?
|
|
30
|
-
|
|
31
|
-
issues.first
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def issue_title_needs_updating?(issue, test)
|
|
35
|
-
issue.title.strip != title_from_test(test) && !%w[canary production preprod release].include?(pipeline)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def new_issue_labels(_test)
|
|
39
|
-
%w[E2E status::automated suppress-contributor-links]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
|
43
|
-
labels = super
|
|
44
|
-
labels |= new_issue_labels(test).to_set
|
|
45
|
-
labels.delete_if { |label| label.start_with?("#{pipeline}::") }
|
|
46
|
-
labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def update_issue(issue, test)
|
|
50
|
-
update_params = {}
|
|
51
|
-
|
|
52
|
-
old_title = issue.title.strip
|
|
53
|
-
new_title = title_from_test(test)
|
|
54
|
-
|
|
55
|
-
if old_title != new_title
|
|
56
|
-
update_params[:title] = new_title
|
|
57
|
-
warn(%(#{issue_type} title needs to be updated from '#{old_title}' to '#{new_title}'))
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
old_description = issue.description
|
|
61
|
-
new_description = updated_description(issue, test)
|
|
62
|
-
|
|
63
|
-
if old_description != new_description
|
|
64
|
-
warn(%(#{issue_type} description needs to be updated from '#{old_description}' to '#{new_description}'))
|
|
65
|
-
update_params[:description] = new_description
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
return if update_params.empty?
|
|
69
|
-
|
|
70
|
-
gitlab.edit_issue(iid: issue.iid, options: update_params)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def search_term(test)
|
|
76
|
-
%("#{partial_file_path(test.file)}" "#{search_safe(test.name)}")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def search_for_issues(test)
|
|
80
|
-
gitlab.find_issues(options: { search: search_term(test) }) do |issue|
|
|
81
|
-
issue.state == 'opened' && issue.issue_type == issue_type && issue.title.strip == title_from_test(test)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GitlabQuality
|
|
4
|
-
module TestTooling
|
|
5
|
-
module Report
|
|
6
|
-
# Uses the API to:
|
|
7
|
-
# - create or update test cases
|
|
8
|
-
# - create or update issues
|
|
9
|
-
# based on the results of tests from RSpec run result files.
|
|
10
|
-
class ReportResults < ReportAsIssue
|
|
11
|
-
attr_accessor :testcase_project_reporter, :results_issue_project_reporter, :files, :test_case_project,
|
|
12
|
-
:results_issue_project, :gitlab
|
|
13
|
-
|
|
14
|
-
def initialize(
|
|
15
|
-
test_case_project_token:, results_issue_project_token:, input_files:, test_case_project: nil, results_issue_project: nil, dry_run: false,
|
|
16
|
-
**kwargs)
|
|
17
|
-
@test_case_project_token = test_case_project_token
|
|
18
|
-
@testcase_project_reporter = GitlabQuality::TestTooling::Report::ResultsInTestCases.new(
|
|
19
|
-
token: test_case_project_token, input_files: input_files, project: test_case_project, dry_run: dry_run, **kwargs)
|
|
20
|
-
@results_issue_project_reporter = GitlabQuality::TestTooling::Report::ResultsInIssues.new(
|
|
21
|
-
token: results_issue_project_token, input_files: input_files, project: results_issue_project, dry_run: dry_run, **kwargs)
|
|
22
|
-
@test_case_project = test_case_project
|
|
23
|
-
@results_issue_project = results_issue_project
|
|
24
|
-
@files = Array(input_files)
|
|
25
|
-
@gitlab = testcase_project_reporter.gitlab
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def validate_input!
|
|
29
|
-
assert_input_files!(files)
|
|
30
|
-
gitlab.assert_user_permission!
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
attr_reader :test_case_project_token
|
|
36
|
-
|
|
37
|
-
# rubocop:disable Metrics/AbcSize
|
|
38
|
-
def run!
|
|
39
|
-
puts "Reporting test results in `#{files.join(',')}` as test cases in project `#{test_case_project}` " \
|
|
40
|
-
"and issues in project `#{results_issue_project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
|
41
|
-
|
|
42
|
-
TestResults::Builder.new(token: test_case_project_token, project: test_case_project, file_glob: files).test_results_per_file do |test_results|
|
|
43
|
-
puts "Reporting tests in #{test_results.path}"
|
|
44
|
-
|
|
45
|
-
test_results.each do |test|
|
|
46
|
-
next if test.file.include?('/features/sanity/') || test.skipped?
|
|
47
|
-
|
|
48
|
-
puts "Reporting test: #{test.relative_file} | #{test.name}\n"
|
|
49
|
-
|
|
50
|
-
report_test(test)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def report_test(test)
|
|
56
|
-
testcase = testcase_project_reporter.find_or_create_testcase(test)
|
|
57
|
-
|
|
58
|
-
result_issue, is_new = results_issue_project_reporter.get_related_issue(testcase, test)
|
|
59
|
-
|
|
60
|
-
testcase_project_reporter.add_result_issue_link_to_testcase(testcase, result_issue, test) if is_new
|
|
61
|
-
|
|
62
|
-
testcase_project_reporter.update_testcase(testcase, test)
|
|
63
|
-
|
|
64
|
-
labels_updated = results_issue_project_reporter.update_issue_labels(result_issue, test)
|
|
65
|
-
note_posted = results_issue_project_reporter.post_note(result_issue, test)
|
|
66
|
-
|
|
67
|
-
if labels_updated || note_posted
|
|
68
|
-
puts "Issue updated: #{result_issue.web_url}"
|
|
69
|
-
else
|
|
70
|
-
puts "Test passed, no results issue update needed."
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
# rubocop:enable Metrics/AbcSize
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GitlabQuality
|
|
4
|
-
module TestTooling
|
|
5
|
-
module Report
|
|
6
|
-
# Uses the API to create or update GitLab test result issues with the results of tests from RSpec report files.
|
|
7
|
-
class ResultsInIssues < ReportAsIssue
|
|
8
|
-
include Concerns::ResultsReporter
|
|
9
|
-
|
|
10
|
-
def initialize(**kwargs)
|
|
11
|
-
super
|
|
12
|
-
@issue_type = 'issue'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def get_related_issue(testcase, test)
|
|
16
|
-
issue = find_linked_results_issue_by_iid(testcase, test)
|
|
17
|
-
is_new = false
|
|
18
|
-
|
|
19
|
-
if issue
|
|
20
|
-
issue = update_issue(issue, test) if issue_title_needs_updating?(issue, test)
|
|
21
|
-
else
|
|
22
|
-
puts "No valid issue link found."
|
|
23
|
-
issue = find_or_create_results_issue(test)
|
|
24
|
-
is_new = true
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
[issue, is_new]
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def update_issue_labels(issue, test)
|
|
31
|
-
new_labels = issue_labels(issue)
|
|
32
|
-
new_labels |= ['Testcase Linked']
|
|
33
|
-
|
|
34
|
-
update_labels(issue, test, new_labels)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def post_note(issue, test)
|
|
38
|
-
return false if test.skipped?
|
|
39
|
-
return false if test.failures.empty?
|
|
40
|
-
|
|
41
|
-
note = note_content(test)
|
|
42
|
-
|
|
43
|
-
gitlab.find_issue_discussions(iid: issue.iid).each do |discussion|
|
|
44
|
-
next unless new_note_matches_discussion?(note, discussion)
|
|
45
|
-
|
|
46
|
-
gitlab.add_note_to_issue_discussion_as_thread(
|
|
47
|
-
iid: issue.iid,
|
|
48
|
-
discussion_id: discussion.id,
|
|
49
|
-
note: failure_summary)
|
|
50
|
-
return true
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
gitlab.create_issue_note(iid: issue.iid, note: note)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def find_linked_results_issue_by_iid(testcase, _test)
|
|
59
|
-
iid = issue_iid_from_testcase(testcase)
|
|
60
|
-
|
|
61
|
-
return unless iid
|
|
62
|
-
|
|
63
|
-
find_issue_by_iid(iid)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def find_or_create_results_issue(test)
|
|
67
|
-
find_issue(test) || create_issue(test)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def issue_iid_from_testcase(testcase)
|
|
71
|
-
results = testcase.description.partition(TEST_CASE_RESULTS_SECTION_TEMPLATE).last if testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE)
|
|
72
|
-
|
|
73
|
-
return unless results
|
|
74
|
-
|
|
75
|
-
issue_iid = results.split('/').last
|
|
76
|
-
|
|
77
|
-
issue_iid&.to_i
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def note_content(test)
|
|
81
|
-
errors = test.failures.each_with_object([]) do |failure, text|
|
|
82
|
-
text << <<~TEXT
|
|
83
|
-
Error:
|
|
84
|
-
```
|
|
85
|
-
#{failure['message']}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Stacktrace:
|
|
89
|
-
```
|
|
90
|
-
#{failure['stacktrace']}
|
|
91
|
-
```
|
|
92
|
-
TEXT
|
|
93
|
-
end.join("\n\n")
|
|
94
|
-
|
|
95
|
-
"#{failure_summary}\n\n#{errors}"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def failure_summary
|
|
99
|
-
summary = [%(:x: ~"#{pipeline}::failed")]
|
|
100
|
-
summary << "in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}"
|
|
101
|
-
summary.join(' ')
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def new_note_matches_discussion?(note, discussion)
|
|
105
|
-
note_error = error_and_stack_trace(note)
|
|
106
|
-
discussion_error = error_and_stack_trace(discussion.notes.first['body'])
|
|
107
|
-
|
|
108
|
-
return false if note_error.empty? || discussion_error.empty?
|
|
109
|
-
|
|
110
|
-
note_error == discussion_error
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def error_and_stack_trace(text)
|
|
114
|
-
text.strip[/Error:(.*)/m, 1].to_s
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def updated_description(_issue, test)
|
|
118
|
-
new_issue_description(test)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GitlabQuality
|
|
4
|
-
module TestTooling
|
|
5
|
-
module Report
|
|
6
|
-
# Uses the API to create or update GitLab test cases with the results of tests from RSpec report files.
|
|
7
|
-
class ResultsInTestCases < ReportAsIssue
|
|
8
|
-
include Concerns::ResultsReporter
|
|
9
|
-
|
|
10
|
-
attr_reader :issue_type, :gitlab
|
|
11
|
-
|
|
12
|
-
def initialize(**kwargs)
|
|
13
|
-
super
|
|
14
|
-
@issue_type = 'test_case'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def find_or_create_testcase(test)
|
|
18
|
-
find_testcase(test) || create_issue(test)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def add_result_issue_link_to_testcase(testcase, result_issue, _test)
|
|
22
|
-
results_section = testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE) ? '' : TEST_CASE_RESULTS_SECTION_TEMPLATE
|
|
23
|
-
|
|
24
|
-
gitlab.edit_issue(iid: testcase.iid,
|
|
25
|
-
options: { description: (testcase.description + results_section + "\n\n#{result_issue.web_url}") })
|
|
26
|
-
puts "Added results issue #{result_issue.web_url} link to test case #{testcase.web_url}"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def update_testcase(testcase, test)
|
|
30
|
-
puts "Labels updated for test case #{testcase.web_url}." if update_labels(testcase, test)
|
|
31
|
-
puts "Quarantine section updated for test case #{testcase.web_url}." if update_quarantine_section(testcase, test)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def find_testcase(test)
|
|
37
|
-
testcase = find_issue(test)
|
|
38
|
-
return unless testcase
|
|
39
|
-
|
|
40
|
-
if testcase_needs_updating?(testcase, test)
|
|
41
|
-
update_issue(testcase, test)
|
|
42
|
-
else
|
|
43
|
-
testcase
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def new_issue_description(test)
|
|
48
|
-
quarantine_section = test.quarantine? && test.quarantine_issue ? "\n\n### Quarantine issue\n\n#{test.quarantine_issue}" : ''
|
|
49
|
-
|
|
50
|
-
"#{super}#{quarantine_section}\n\n#{execution_graph_section(test)}"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def execution_graph_section(test)
|
|
54
|
-
formatted_path = CGI.escape(test.relative_file)
|
|
55
|
-
|
|
56
|
-
<<~MKDOWN.strip
|
|
57
|
-
### Executions
|
|
58
|
-
|
|
59
|
-
[Spec metrics on all environments](https://dashboards.devex.gitlab.net/d/739c1bdd-a436-452b-bddc-fccb4d055768/single-test-overview?var-file_path=#{formatted_path})
|
|
60
|
-
MKDOWN
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def updated_description(testcase, test)
|
|
64
|
-
historical_results_section = testcase.description.match(/### DO NOT EDIT BELOW THIS LINE[\s\S]+/)
|
|
65
|
-
|
|
66
|
-
"#{new_issue_description(test)}\n\n#{historical_results_section}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def testcase_needs_updating?(testcase, test)
|
|
70
|
-
return false if %w[canary production preprod release].include?(pipeline)
|
|
71
|
-
return true if issue_title_needs_updating?(testcase, test)
|
|
72
|
-
|
|
73
|
-
!testcase.description.include?(execution_graph_section(test))
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def quarantine_section_needs_updating?(testcase, test)
|
|
77
|
-
if test.quarantine? && test.quarantine_issue
|
|
78
|
-
return false if testcase.description.include?(test.quarantine_issue)
|
|
79
|
-
else
|
|
80
|
-
return false unless testcase.description.include?('Quarantine issue')
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
true
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def update_quarantine_section(testcase, test)
|
|
87
|
-
return unless quarantine_section_needs_updating?(testcase, test)
|
|
88
|
-
|
|
89
|
-
new_description = updated_description(testcase, test)
|
|
90
|
-
|
|
91
|
-
gitlab.edit_issue(iid: testcase.iid, options: { description: new_description })
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|