gitlab_quality-test_tooling 2.9.0 → 2.11.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/.rspec +2 -1
- data/Gemfile.lock +9 -1
- data/README.md +44 -3
- data/exe/failed-test-issues +53 -8
- data/exe/feature-readiness-checklist +61 -0
- data/exe/feature-readiness-evaluation +62 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_epic.rb +94 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_issue.rb +92 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_merge_request.rb +139 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +34 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +128 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/evaluation.rb +82 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb +82 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_graphql_client.rb +54 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +38 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +3 -3
- data/lib/gitlab_quality/test_tooling/gitlab_client/labels_client.rb +13 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +21 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +0 -10
- data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_client.rb +277 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_dry_client.rb +25 -0
- data/lib/gitlab_quality/test_tooling/labels_inference.rb +4 -0
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +6 -6
- data/lib/gitlab_quality/test_tooling/report/feature_readiness/report_on_epic.rb +174 -0
- data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +120 -20
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +11 -6
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +18 -5
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c38aa70a45607f4aa4fb6b5a30370451b4220983ff420e05bac40260bc361c70
|
4
|
+
data.tar.gz: 25fa008b29d0d8470efbd5ed2f5029b693031ab9b8fddba6884a9d4075241ee4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36061bbe33b1a7bab05481ccf8cebe1146a27846862373a6178cedddf3cc7f73ced1d7e98197a753650d5a82ea25ea4886b27f74f6fca13a2986d699e35a7b16
|
7
|
+
data.tar.gz: 54612a3243704341c2b19c3f6141cca89c88c2e62189b074643da8f5b7527008feb8df525b426a05daddd7107aec24bae32a36950a862dba8230e4099aadbc33
|
data/.rspec
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitlab_quality-test_tooling (2.
|
4
|
+
gitlab_quality-test_tooling (2.11.0)
|
5
5
|
activesupport (>= 7.0, < 7.3)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
fog-google (~> 1.24, >= 1.24.1)
|
@@ -187,6 +187,7 @@ GEM
|
|
187
187
|
http-cookie (~> 1.0)
|
188
188
|
http-form_data (~> 2.2)
|
189
189
|
llhttp-ffi (~> 0.5.0)
|
190
|
+
http-accept (1.7.0)
|
190
191
|
http-cookie (1.0.7)
|
191
192
|
domain_name (~> 0.5)
|
192
193
|
http-form_data (2.3.0)
|
@@ -233,6 +234,7 @@ GEM
|
|
233
234
|
nenv (0.3.0)
|
234
235
|
net-http (0.4.1)
|
235
236
|
uri
|
237
|
+
netrc (0.11.0)
|
236
238
|
nokogiri (1.16.7)
|
237
239
|
mini_portile2 (~> 2.8.2)
|
238
240
|
racc (~> 1.4)
|
@@ -273,6 +275,11 @@ GEM
|
|
273
275
|
declarative (< 0.1.0)
|
274
276
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
275
277
|
uber (< 0.2.0)
|
278
|
+
rest-client (2.1.0)
|
279
|
+
http-accept (>= 1.7.0, < 2.0)
|
280
|
+
http-cookie (>= 1.0.2, < 2.0)
|
281
|
+
mime-types (>= 1.16, < 4.0)
|
282
|
+
netrc (~> 0.8)
|
276
283
|
retriable (3.1.2)
|
277
284
|
reverse_markdown (2.1.1)
|
278
285
|
nokogiri
|
@@ -408,6 +415,7 @@ DEPENDENCIES
|
|
408
415
|
lefthook (~> 1.3)
|
409
416
|
pry-byebug (= 3.10.1)
|
410
417
|
rake (~> 13.0)
|
418
|
+
rest-client (~> 2.1.0)
|
411
419
|
rspec (~> 3.12)
|
412
420
|
rspec_junit_formatter (~> 0.6.0)
|
413
421
|
simplecov (~> 0.22)
|
data/README.md
CHANGED
@@ -161,9 +161,17 @@ Usage: exe/knapsack-report-issues [options]
|
|
161
161
|
```shell
|
162
162
|
Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)
|
163
163
|
Usage: exe/failed-test-issues [options]
|
164
|
-
-i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
|
165
|
-
-p, --project PROJECT Can be an integer or a group/project string
|
166
|
-
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
164
|
+
-i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML) [REQUIRED]
|
165
|
+
-p, --project PROJECT Can be an integer or a group/project string [REQUIRED]
|
166
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT [REQUIRED]
|
167
|
+
--enable-issue-update [BOOLEAN]
|
168
|
+
Enable/disable test health issue creation/update (default: true)
|
169
|
+
--enable-gcs [BOOLEAN] Enable/disable the push of JSON data to a Google Cloud Storage (GCS) bucket (default: false)
|
170
|
+
--gcs-project-id GCS_PROJECT_ID
|
171
|
+
Google Cloud project ID for GCS bucket access
|
172
|
+
--gcs-bucket GCS_BUCKET Name of the GCS bucket to store metrics data
|
173
|
+
--gcs-credentials GCS_CREDENTIALS
|
174
|
+
GCS service account credentials (file path or string)
|
167
175
|
--max-diff-ratio MAX_DIFF_RATO
|
168
176
|
Max stacktrace diff ratio for failure issues detection
|
169
177
|
-r RELATED_ISSUES_FILE, The file path for the related issues
|
@@ -249,6 +257,39 @@ Usage: exe/update-test-meta [options]
|
|
249
257
|
-h, --help Show the usage
|
250
258
|
```
|
251
259
|
|
260
|
+
### `exe/feature-readiness-checklist`
|
261
|
+
|
262
|
+
```shell
|
263
|
+
Purpose: Conditionally create operational readiness checklist and link it to the epic
|
264
|
+
Usage: exe/feature-readiness-checklist [options]
|
265
|
+
-p, --project PROJECT Name of the project
|
266
|
+
-g, --group GROUP Name of the group
|
267
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
268
|
+
-l, --search_labels LABELS A comma seperated list of labels to filter the epics on
|
269
|
+
-m, --minutes MINUTES Limit the search to issues and epics created with last MINUTES. Optional.
|
270
|
+
-b, --blocking Block the epic by any issues created and linked to it
|
271
|
+
--dry-run Perform a dry-run (don't create branches, commits or MRs)
|
272
|
+
-v, --version Show the version
|
273
|
+
-h, --help Show the usage
|
274
|
+
```
|
275
|
+
|
276
|
+
### `exe/feature-readiness-evaluation`
|
277
|
+
|
278
|
+
```shell
|
279
|
+
Purpose: Evaluate MRs for multiple feature readiness criteria and report the status to the corresponding epic
|
280
|
+
Usage: exe/feature-readiness-evaluation [options]
|
281
|
+
-p, --project PROJECT Name of the project
|
282
|
+
-g, --group GROUP Name of the group
|
283
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
284
|
+
-l, --search_labels LABELS A comma seperated list of labels to filter the epics on
|
285
|
+
-m, --minutes MINUTES Limit the search to issues and epics created with last MINUTES. Optional.
|
286
|
+
-e, --epic-iid EPIC_IID evaluate an epic with iid
|
287
|
+
-o, --operational-readinessr Perform the operational readiness checklist automation
|
288
|
+
--dry-run Perform a dry-run (don't create branches, commits or MRs)
|
289
|
+
-v, --version Show the version
|
290
|
+
-h, --help Show the usage
|
291
|
+
```
|
292
|
+
|
252
293
|
## Development
|
253
294
|
|
254
295
|
### Initial setup
|
data/exe/failed-test-issues
CHANGED
@@ -9,20 +9,48 @@ require_relative "../lib/gitlab_quality/test_tooling"
|
|
9
9
|
params = {}
|
10
10
|
|
11
11
|
options = OptionParser.new do |opts|
|
12
|
+
def to_boolean(value, default)
|
13
|
+
return default if value.nil?
|
14
|
+
return true if value == true || value.to_s.casecmp('true').zero?
|
15
|
+
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
12
19
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
13
20
|
|
14
|
-
opts.on('-i', '--input-files INPUT_FILES', String, 'RSpec report files (JSON or JUnit XML)') do |input_files|
|
21
|
+
opts.on('-i', '--input-files INPUT_FILES', String, 'RSpec report files (JSON or JUnit XML) [REQUIRED]') do |input_files|
|
15
22
|
params[:input_files] = input_files
|
16
23
|
end
|
17
24
|
|
18
|
-
opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
|
25
|
+
opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string [REQUIRED]') do |project|
|
19
26
|
params[:project] = project
|
20
27
|
end
|
21
28
|
|
22
|
-
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
29
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT [REQUIRED]') do |token|
|
23
30
|
params[:token] = token
|
24
31
|
end
|
25
32
|
|
33
|
+
opts.on('--enable-issue-update [BOOLEAN]', "Enable/disable test health issue creation/update (default: true)") do |toggle|
|
34
|
+
params[:issue_update_enabled] = to_boolean(toggle, true)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Google Cloud Storage (GCS) options
|
38
|
+
opts.on('--enable-gcs [BOOLEAN]', "Enable/disable the push of JSON data to a Google Cloud Storage (GCS) bucket (default: false)") do |toggle|
|
39
|
+
params[:gcs_enabled] = to_boolean(toggle, false)
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on('--gcs-project-id GCS_PROJECT_ID', String, 'Google Cloud project ID for GCS bucket access') do |project_id|
|
43
|
+
params[:gcs_project_id] = project_id
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on('--gcs-bucket GCS_BUCKET', String, 'Name of the GCS bucket to store metrics data') do |bucket|
|
47
|
+
params[:gcs_bucket] = bucket
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('--gcs-credentials GCS_CREDENTIALS', String, 'GCS service account credentials (file path or string)') do |credentials|
|
51
|
+
params[:gcs_credentials] = credentials
|
52
|
+
end
|
53
|
+
|
26
54
|
opts.on('--max-diff-ratio MAX_DIFF_RATO', Float, 'Max stacktrace diff ratio for failure issues detection') do |max_diff_ratio|
|
27
55
|
params[:max_diff_ratio] = max_diff_ratio
|
28
56
|
end
|
@@ -56,13 +84,30 @@ options = OptionParser.new do |opts|
|
|
56
84
|
puts opts
|
57
85
|
exit
|
58
86
|
end
|
59
|
-
|
60
|
-
opts.parse(ARGV)
|
61
87
|
end
|
62
88
|
|
63
|
-
|
89
|
+
begin
|
90
|
+
options.parse!(ARGV)
|
91
|
+
|
92
|
+
required_options = [:input_files, :project, :token]
|
93
|
+
missing_options = required_options.select { |option| params[option].nil? }
|
94
|
+
|
95
|
+
if params[:gcs_enabled]
|
96
|
+
gcs_required_options = [:gcs_project_id, :gcs_bucket, :gcs_credentials]
|
97
|
+
missing_gcs_options = gcs_required_options.select { |option| params[option].nil? }
|
98
|
+
|
99
|
+
missing_options.concat(missing_gcs_options) if missing_gcs_options.any?
|
100
|
+
end
|
101
|
+
|
102
|
+
if missing_options.any?
|
103
|
+
warn "Error: Missing required options: #{missing_options.map { |o| "--#{o.to_s.tr('_', '-')}" }.join(', ')}\n"
|
104
|
+
warn options
|
105
|
+
exit 1
|
106
|
+
end
|
107
|
+
|
64
108
|
GitlabQuality::TestTooling::Report::FailedTestIssue.new(**params).invoke!
|
65
|
-
|
66
|
-
|
109
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
110
|
+
warn "Error: #{e.message}"
|
111
|
+
warn options
|
67
112
|
exit 1
|
68
113
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "optparse"
|
5
|
+
|
6
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
7
|
+
params = {}
|
8
|
+
|
9
|
+
options = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
11
|
+
|
12
|
+
opts.on('-p', '--project PROJECT', String, 'Name of the project') do |project|
|
13
|
+
params[:project] = project
|
14
|
+
end
|
15
|
+
opts.on('-g', '--group GROUP', String, 'Name of the group') do |group|
|
16
|
+
params[:group] = group
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
20
|
+
params[:token] = token
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('-l', '--search_labels LABELS', String, 'A comma seperated list of labels to filter the epics on') do |search_labels|
|
24
|
+
params[:search_labels] = search_labels.split(',')
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-m', '--minutes MINUTES', Integer, 'Limit the search to issues and epics created with last MINUTES. Optional.') do |minutes|
|
28
|
+
params[:limit_to_minutes] = minutes
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-b', '--blocking', 'Block the epic by any issues created and linked to it') do
|
32
|
+
params[:issue_is_blocking] = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on('--dry-run', "Perform a dry-run (don't create branches, commits or MRs)") do
|
36
|
+
params[:dry_run] = true
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
40
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
41
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
46
|
+
puts "Purpose: Conditionally create operational readiness checklist and link it to the epic"
|
47
|
+
puts opts
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.parse(ARGV)
|
52
|
+
end
|
53
|
+
|
54
|
+
raise ArgumentError, "Missing argument(s). Required arguments are: --project, --group, --token" if params.empty? || ([:project, :group, :token] - params.keys).any?
|
55
|
+
|
56
|
+
if params.any?
|
57
|
+
GitlabQuality::TestTooling::FeatureReadiness::OperationalReadinessCheck.new(**params).invoke!
|
58
|
+
else
|
59
|
+
puts options
|
60
|
+
exit 1
|
61
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "optparse"
|
5
|
+
require 'active_support/core_ext/hash'
|
6
|
+
|
7
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
8
|
+
params = {}
|
9
|
+
|
10
|
+
options = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
12
|
+
|
13
|
+
opts.on('-p', '--project PROJECT', String, 'Name of the project') do |project|
|
14
|
+
params[:project] = project
|
15
|
+
end
|
16
|
+
opts.on('-g', '--group GROUP', String, 'Name of the group') do |group|
|
17
|
+
params[:group] = group
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
21
|
+
params[:token] = token
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on('-l', '--search_labels LABELS', String, 'A comma seperated list of labels to filter the epics on') do |search_labels|
|
25
|
+
params[:search_labels] = search_labels.split(',')
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('-m', '--minutes MINUTES', Integer, 'Limit the search to issues and epics created with last MINUTES. Optional.') do |minutes|
|
29
|
+
params[:limit_to_minutes] = minutes
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('-ei', '--epic-iid EPIC_IID', String, 'evaluate an epic with iid') do |epic_iid|
|
33
|
+
params[:epic_iid] = epic_iid
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('--dry-run', "Perform a dry-run (don't create branches, commits or MRs)") do
|
37
|
+
params[:dry_run] = true
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
41
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
42
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
47
|
+
puts "Purpose: Evaluate MRs for multiple feature readiness criteria and report the status to the corresponding epic"
|
48
|
+
puts opts
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.parse(ARGV)
|
53
|
+
end
|
54
|
+
|
55
|
+
raise ArgumentError, "Missing argument(s). Required arguments are: --project, --group, --token" if params.empty? || ([:project, :group, :token] - params.keys).any?
|
56
|
+
|
57
|
+
if params.any?
|
58
|
+
GitlabQuality::TestTooling::FeatureReadiness::Evaluation.new(**params).invoke!
|
59
|
+
else
|
60
|
+
puts options
|
61
|
+
exit 1
|
62
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module FeatureReadiness
|
6
|
+
module AnalyzedItems
|
7
|
+
class AnalyzedEpic
|
8
|
+
include Concerns::WorkItemConcern
|
9
|
+
|
10
|
+
def initialize(epic:, token:, project:, group:, dry_run:)
|
11
|
+
@token = token
|
12
|
+
@project = project
|
13
|
+
@group = group
|
14
|
+
@dry_run = dry_run
|
15
|
+
@epic = epic
|
16
|
+
@analyzed_issues = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def analyze
|
20
|
+
issue_iids = get_issue_iids(epic, project)
|
21
|
+
return if issue_iids.none?
|
22
|
+
|
23
|
+
open_issues = fetch_open_issues(issue_iids)
|
24
|
+
|
25
|
+
puts "\nProcessing epic: #{epic[:webUrl]} with #{open_issues.count} open feature addition issues."
|
26
|
+
|
27
|
+
process_issues(open_issues)
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_issues(issues)
|
31
|
+
@analyzed_issues.concat(
|
32
|
+
issues.map do |issue|
|
33
|
+
AnalyzedIssue.new(issue: issue, token: token, project: project, group: group, dry_run: dry_run)
|
34
|
+
.tap(&:analyze)
|
35
|
+
.result
|
36
|
+
end
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def result
|
41
|
+
{
|
42
|
+
epic_iid: epic[:iid],
|
43
|
+
epic_id: epic[:id],
|
44
|
+
epic_web_url: epic[:webUrl],
|
45
|
+
issues: analyzed_issues,
|
46
|
+
doc_mrs: doc_mrs,
|
47
|
+
feature_spec_mrs: feature_spec_mrs,
|
48
|
+
e2e_spec_mrs: e2e_spec_mrs,
|
49
|
+
feature_flag_mrs: feature_flag_mrs
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_reader :epic, :token, :project, :dry_run, :group, :analyzed_issues
|
56
|
+
|
57
|
+
def work_items_client
|
58
|
+
@work_items_client ||= (dry_run ? GitlabClient::WorkItemsDryClient : GitlabClient::WorkItemsClient).new(token: token, project: project, group: group)
|
59
|
+
end
|
60
|
+
|
61
|
+
def issue_client
|
62
|
+
@issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_open_issues(issue_iids)
|
66
|
+
issue_client.find_issues(
|
67
|
+
options: {
|
68
|
+
iids: issue_iids,
|
69
|
+
labels: Evaluation::BASE_LABELS_FOR_SEARCH,
|
70
|
+
state: 'opened'
|
71
|
+
}
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def doc_mrs
|
76
|
+
analyzed_issues.flat_map { |issue| issue[:doc_mrs].any? ? issue[:doc_mrs] : [] }
|
77
|
+
end
|
78
|
+
|
79
|
+
def feature_spec_mrs
|
80
|
+
analyzed_issues.flat_map { |issue| issue[:feature_spec_mrs].any? ? issue[:feature_spec_mrs] : [] }
|
81
|
+
end
|
82
|
+
|
83
|
+
def e2e_spec_mrs
|
84
|
+
analyzed_issues.flat_map { |issue| issue[:e2e_spec_mrs].any? ? issue[:e2e_spec_mrs] : [] }
|
85
|
+
end
|
86
|
+
|
87
|
+
def feature_flag_mrs
|
88
|
+
analyzed_issues.flat_map { |issue| issue[:feature_flag_mrs].any? ? issue[:feature_flag_mrs] : [] }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module FeatureReadiness
|
6
|
+
module AnalyzedItems
|
7
|
+
class AnalyzedIssue
|
8
|
+
def initialize(issue:, token:, project:, group:, dry_run:)
|
9
|
+
@issue = issue
|
10
|
+
@token = token
|
11
|
+
@project = project
|
12
|
+
@group = group
|
13
|
+
@dry_run = dry_run
|
14
|
+
@analyzed_merge_requests = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def analyze
|
18
|
+
contributing_mrs = contributing_mrs_for_issue(issue)
|
19
|
+
return if contributing_mrs.empty?
|
20
|
+
|
21
|
+
@analyzed_merge_requests.concat(
|
22
|
+
contributing_mrs
|
23
|
+
.reject { |mr| skip_merge_request?(mr, issue) }
|
24
|
+
.map do |merge_request|
|
25
|
+
AnalyzedMergeRequest.new(merge_request: merge_request, token: token, project: project, group: group, dry_run: dry_run)
|
26
|
+
.tap(&:analyze)
|
27
|
+
.result
|
28
|
+
end
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def result
|
33
|
+
{
|
34
|
+
issue_iid: issue.iid,
|
35
|
+
issue_web_url: issue.web_url,
|
36
|
+
merge_requests: analyzed_merge_requests,
|
37
|
+
doc_mrs: doc_mrs,
|
38
|
+
feature_spec_mrs: feature_spec_mrs,
|
39
|
+
e2e_spec_mrs: e2e_spec_mrs,
|
40
|
+
feature_flag_mrs: feature_flag_mrs
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :issue, :token, :project, :group, :dry_run, :analyzed_merge_requests
|
47
|
+
|
48
|
+
def contributing_mrs_for_issue(issue)
|
49
|
+
related_mrs = issue_client.related_merge_requests(iid: issue.iid)
|
50
|
+
|
51
|
+
return [] if related_mrs.empty?
|
52
|
+
|
53
|
+
related_mrs.select do |mr|
|
54
|
+
mr.description&.include?("##{issue.iid}") || mr.description&.include?(issue.web_url)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def skip_merge_request?(merge_request, issue)
|
59
|
+
if (merge_request.target_project_id != issue.project_id) || (merge_request.draft == true)
|
60
|
+
puts "Skipping: #{merge_request.web_url} #{merge_request.draft == true ? 'as it is a draft' : 'due to different project id'}"
|
61
|
+
return true
|
62
|
+
end
|
63
|
+
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def issue_client
|
68
|
+
@issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
69
|
+
end
|
70
|
+
|
71
|
+
def doc_mrs
|
72
|
+
analyzed_merge_requests
|
73
|
+
.select { |mr| mr[:has_docs] }
|
74
|
+
.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
75
|
+
end
|
76
|
+
|
77
|
+
def feature_spec_mrs
|
78
|
+
analyzed_merge_requests.select { |mr| mr[:has_feature_specs] }.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
79
|
+
end
|
80
|
+
|
81
|
+
def e2e_spec_mrs
|
82
|
+
analyzed_merge_requests.select { |mr| mr[:has_e2e_specs] }.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
83
|
+
end
|
84
|
+
|
85
|
+
def feature_flag_mrs
|
86
|
+
analyzed_merge_requests.select { |mr| mr[:added_feature_flag] }.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_merge_request.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module FeatureReadiness
|
6
|
+
module AnalyzedItems
|
7
|
+
class AnalyzedMergeRequest
|
8
|
+
CODE_DIRECTORIES = %w[app lib config keeps scripts db].freeze
|
9
|
+
ADDITIONS_THRESHOLD = 5
|
10
|
+
CODE_FILES_EXT = "rb|js|vue"
|
11
|
+
SPEC_FILES_EXT = "rb|js"
|
12
|
+
DOC_FILES_EXT = "tmpl|yaml|yml|md"
|
13
|
+
|
14
|
+
def initialize(merge_request:, token:, project:, group:, dry_run:)
|
15
|
+
@merge_request = merge_request
|
16
|
+
@token = token
|
17
|
+
@project = project
|
18
|
+
@group = group
|
19
|
+
@dry_run = dry_run
|
20
|
+
end
|
21
|
+
|
22
|
+
def analyze
|
23
|
+
@files_with_missing_specs ||= fetch_files_with_missing_specs
|
24
|
+
end
|
25
|
+
|
26
|
+
def result
|
27
|
+
{
|
28
|
+
merge_request_iid: merge_request.iid,
|
29
|
+
merge_request_web_url: merge_request.web_url,
|
30
|
+
files_with_missing_specs: files_with_missing_specs,
|
31
|
+
has_docs: has_docs?,
|
32
|
+
has_feature_specs: has_feature_specs?,
|
33
|
+
has_e2e_specs: has_e2e_specs?,
|
34
|
+
added_feature_flag: added_feature_flag?
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :merge_request, :token, :project, :group, :dry_run, :files_with_missing_specs
|
41
|
+
|
42
|
+
def merge_request_client
|
43
|
+
@merge_request_client ||= (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(token: token, project: project)
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_docs?
|
47
|
+
doc_diffs.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_feature_specs?
|
51
|
+
spec_diffs('features/').map { |diff| has_significant_addition?(diff) }.any?
|
52
|
+
end
|
53
|
+
|
54
|
+
def added_feature_flag?
|
55
|
+
filter_diffs(%r{^(ee/)?config/feature_flags/.*\.yml$}).any?(&:new_file)
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_e2e_specs?
|
59
|
+
qa_spec_diffs.map { |diff| has_significant_addition?(diff) }.any?
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch_files_with_missing_specs
|
63
|
+
code_paths_with_missing_specs = []
|
64
|
+
|
65
|
+
CODE_DIRECTORIES.each do |dir|
|
66
|
+
code_diffs = diffs_for_dir(dir, CODE_FILES_EXT)
|
67
|
+
sp_diffs = spec_diffs
|
68
|
+
code_paths_with_missing_specs << collect_paths_with_missing_specs(code_diffs, sp_diffs)
|
69
|
+
end
|
70
|
+
|
71
|
+
code_paths_with_missing_specs.flatten
|
72
|
+
end
|
73
|
+
|
74
|
+
def collect_paths_with_missing_specs(code_diffs, sp_diffs)
|
75
|
+
result_array = []
|
76
|
+
code_diffs.each do |code_diff|
|
77
|
+
result_array << code_diff.new_path if has_significant_addition?(code_diff) && !skip_file?(code_diff.new_path) && !has_matching_spec_for_code?(code_diff, sp_diffs)
|
78
|
+
end
|
79
|
+
|
80
|
+
result_array
|
81
|
+
end
|
82
|
+
|
83
|
+
def has_matching_spec_for_code?(code_diff, sp_diffs)
|
84
|
+
spec_file_names = get_spec_file_names(code_diff.new_path)
|
85
|
+
sp_diffs.any? { |diff| ends_with_names?(diff.new_path, spec_file_names) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def diffs
|
89
|
+
@diffs ||= merge_request_client.merge_request_diffs(merge_request_iid: merge_request.iid)
|
90
|
+
end
|
91
|
+
|
92
|
+
def has_significant_addition?(diff_obj)
|
93
|
+
content_lines = diff_obj.diff.split("\n")
|
94
|
+
|
95
|
+
additions = content_lines.count { |line| line.start_with?("+") }
|
96
|
+
subtractions = content_lines.count { |line| line.start_with?("-") }
|
97
|
+
|
98
|
+
# Return true if additions are above threshold and there are equal or more additions than subtractions
|
99
|
+
additions >= ADDITIONS_THRESHOLD && additions >= subtractions
|
100
|
+
end
|
101
|
+
|
102
|
+
def filter_diffs(pattern)
|
103
|
+
diffs.select { |diff| diff.new_path =~ pattern }
|
104
|
+
end
|
105
|
+
|
106
|
+
def diffs_for_dir(dir, ext)
|
107
|
+
filter_diffs(%r{^(ee/#{Regexp.escape(dir)}|#{Regexp.escape(dir)})/.*\.(#{ext})$})
|
108
|
+
end
|
109
|
+
|
110
|
+
def spec_diffs(spec_sub_dir = '')
|
111
|
+
filter_diffs(%r{^(spec|ee/spec)/#{spec_sub_dir}.+_spec\.(#{SPEC_FILES_EXT})$})
|
112
|
+
end
|
113
|
+
|
114
|
+
def qa_spec_diffs
|
115
|
+
filter_diffs(%r{^qa/qa/specs/features/.+(_spec|_shared_examples|_shared_context)\.rb$})
|
116
|
+
end
|
117
|
+
|
118
|
+
def doc_diffs
|
119
|
+
filter_diffs(%r{^doc/.+\.(#{DOC_FILES_EXT})$}o)
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_spec_file_names(path)
|
123
|
+
filename = File.basename(path, ".*")
|
124
|
+
|
125
|
+
%W[#{filename}_spec #{filename}_shared_examples]
|
126
|
+
end
|
127
|
+
|
128
|
+
def ends_with_names?(path, target_names)
|
129
|
+
[File.basename(path, File.extname(path))].intersect?(target_names)
|
130
|
+
end
|
131
|
+
|
132
|
+
def skip_file?(path)
|
133
|
+
path.end_with?('index.js')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|