gitlab_quality-test_tooling 0.9.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa16a309741761cbce53a73d541cef1c6bb587cd790492cddc02face87e4338b
4
- data.tar.gz: 9cdba5b6c53c83adba07ce9cc0316568b4e8137955fe46faef7705b4b577a5d9
3
+ metadata.gz: 989fdbc7beb98d23c28a6df0918b48db36962183f71e931d90c1ef70cb99b918
4
+ data.tar.gz: 5edeaf64ee7042db858fdec5cd9a434a449f530abeef5a03eafbec873cabf0f4
5
5
  SHA512:
6
- metadata.gz: dbb5472b2c4a5d70b26806c2467faae4a8b6770a2cf0ddc61f27e2c082f51459f5f8f7a4d640ec93ee3a8da0a126f5effdbae174d7aaf71d2054eddee0854529
7
- data.tar.gz: 98bdef30f3db77c15256ed2ef02e9b3550d8ed6d653e932f0550b35ba3b4f99bd899cedd518dd9a3e0e422ad07e232b2ee1eb1a3abfa2713927312a788c17ad5
6
+ metadata.gz: 936dc55126b3e217b38a43cb7b5b6810bc01f6bbc74d8713414818422130a9158d779941a6f49c11a4ca7e290922cf8c53c19717d1164a0b55e9bc4ee1481aa9
7
+ data.tar.gz: ebe5967dc286b06b72ed8efba01d3c713019e7e89c8b3bf30d26f6567832a4f939b0e4e15d39020acfa959352b3200a996562eb2b3bf43a40009ad6f4c96259b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (0.9.3)
4
+ gitlab_quality-test_tooling (1.0.0)
5
5
  activesupport (>= 6.1, < 7.1)
6
6
  gitlab (~> 4.19)
7
7
  http (~> 5.0)
data/README.md CHANGED
@@ -128,11 +128,28 @@ Purpose: Create slow test issues from JSON RSpec report files
128
128
  Usage: exe/slow-test-issue [options]
129
129
  -i, --input-files INPUT_FILES JSON RSpec report files JSON
130
130
  -p, --project PROJECT Can be an integer or a group/project string
131
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
132
+ --dry-run Perform a dry-run (don't create note)
133
+ -v, --version Show the version
134
+ -h, --help Show the usage
135
+ ```
136
+
137
+ ### `slow-test-merge-request-report-note`
138
+
139
+ ```shell
140
+ $ exe/slow-test-merge-request-report-note -h
141
+ Purpose: Create slow test note on merge requests from JSON RSpec report files
142
+ Usage: exe/slow-test-merge-request-report-note [options]
143
+ -i, --input-files INPUT_FILES JSON RSpec report files JSON
144
+ --project PROJECT Can be an integer or a group/project string
145
+ -m MERGE_REQUEST_IID, An integer merge request IID
146
+ --merge_request_iid
131
147
  -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
132
148
  --dry-run Perform a dry-run (don't create issues)
133
149
  -v, --version Show the version
134
150
  -h, --help Show the usage
135
151
  ```
152
+
136
153
  ## Development
137
154
 
138
155
  ### Initial setup
@@ -0,0 +1,54 @@
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, 'JSON RSpec report files JSON') do |input_files|
15
+ params[:input_files] = input_files
16
+ end
17
+
18
+ opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
19
+ params[:project] = project
20
+ end
21
+
22
+ opts.on('-m', '--merge_request_iid MERGE_REQUEST_IID', String, 'An integer merge request IID') do |merge_request_iid|
23
+ params[:merge_request_iid] = merge_request_iid
24
+ end
25
+
26
+ opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
27
+ params[:token] = token
28
+ end
29
+
30
+ opts.on('--dry-run', "Perform a dry-run (don't create note)") do
31
+ params[:dry_run] = true
32
+ end
33
+
34
+ opts.on_tail('-v', '--version', 'Show the version') do
35
+ require_relative "../lib/gitlab_quality/test_tooling/version"
36
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
37
+ exit
38
+ end
39
+
40
+ opts.on_tail('-h', '--help', 'Show the usage') do
41
+ puts "Purpose: Create slow test note on merge requests from JSON RSpec report files"
42
+ puts opts
43
+ exit
44
+ end
45
+
46
+ opts.parse(ARGV)
47
+ end
48
+
49
+ if params.any?
50
+ GitlabQuality::TestTooling::Report::MergeRequestSlowTestsReport.new(**params).invoke!
51
+ else
52
+ puts options
53
+ exit 1
54
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gitlab'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module GitlabClient
8
+ class MergeRequest
9
+ def initialize(token:, project:, merge_request_iid:)
10
+ @token = token
11
+ @project = project
12
+ @merge_request_iid = merge_request_iid
13
+ end
14
+
15
+ def find_merge_request
16
+ client.merge_request_changes(project, merge_request_iid)
17
+ end
18
+
19
+ def merge_request_changed_files
20
+ find_merge_request["changes"].map do |change|
21
+ change["new_path"]
22
+ end
23
+ end
24
+
25
+ def find_note(body:)
26
+ client.merge_request_notes(project, merge_request_iid, per_page: 100).auto_paginate.find do |mr_note|
27
+ mr_note['body'] =~ /#{body}/
28
+ end
29
+ end
30
+
31
+ def create_note(note:)
32
+ client.create_merge_request_note(project, merge_request_iid, note)
33
+ end
34
+
35
+ def update_note(id:, note:)
36
+ client.edit_merge_request_note(project, merge_request_iid, id, note)
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :project, :token, :merge_request_iid
42
+
43
+ def client
44
+ @client ||= Gitlab.client(
45
+ endpoint: Runtime::Env.gitlab_api_base,
46
+ private_token: token
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module GitlabClient
6
+ class MergeRequestDryClient < MergeRequest
7
+ def find_merge_request
8
+ puts "Finding merge_request_id #{merge_request_iid}"
9
+ puts "project: #{project}"
10
+ end
11
+
12
+ def merge_request_changed_files
13
+ puts "Changed files for #{merge_request_iid}"
14
+ []
15
+ end
16
+
17
+ def find_note(body:)
18
+ puts "Find note for #{merge_request_iid} with body: #{body}"
19
+ end
20
+
21
+ def create_note(note:)
22
+ puts "The following note would have been created with body: #{note}"
23
+ end
24
+
25
+ def update_note(id:, note:)
26
+ puts "The following note would have been update id: #{id} with body: #{note}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -39,9 +39,7 @@ module GitlabQuality
39
39
 
40
40
  def fetch_group_sets(product_group)
41
41
  @group_sets = @stage_sets.select do |user|
42
- user['role'].include?(product_group.split("_").map do |word|
43
- word == 'and' ? word : word.capitalize
44
- end.join(" "))
42
+ user['role'].downcase.tr(' ', '_').include?(product_group)
45
43
  end
46
44
  end
47
45
  end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Report
6
+ class MergeRequestSlowTestsReport
7
+ SLOW_TEST_MESSAGE = '<!-- slow-test -->'
8
+ SLOW_TEST_LABEL = '/label ~"rspec:slow test detected"'
9
+
10
+ def initialize(token:, input_files:, merge_request_iid:, project: nil, dry_run: false, **_kwargs)
11
+ @project = project
12
+ @gitlab_merge_request = (dry_run ? GitlabClient::MergeRequestDryClient : GitlabClient::MergeRequest).new(token: token, project: project,
13
+ merge_request_iid: merge_request_iid)
14
+ @files = Array(input_files)
15
+ @merge_request_iid = merge_request_iid
16
+ end
17
+
18
+ def invoke!
19
+ validate_input!
20
+
21
+ run!
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :gitlab_merge_request, :files, :project, :merge_request_iid
27
+
28
+ def run!
29
+ puts "Reporting slow tests in MR #{merge_request_iid}"
30
+
31
+ TestResults::Builder.new(files).test_results_per_file do |test_results|
32
+ puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
33
+
34
+ upsert_mr_note(slow_related_tests(slow_tests(test_results))) if should_create_mr_note?(test_results)
35
+ end
36
+ end
37
+
38
+ def should_create_mr_note?(test_results)
39
+ gitlab_merge_request.merge_request_changed_files.any? &&
40
+ slow_tests(test_results).any? &&
41
+ slow_related_tests(slow_tests(test_results)).any?
42
+ end
43
+
44
+ def slow_related_tests(slow_test_results)
45
+ slow_test_results.select do |slow_test_result|
46
+ base_file = slow_test_result.file.split('/').last.gsub('_spec.rb', '')
47
+
48
+ merge_request_changed_files.any? { |change| change =~ /#{base_file}/ }
49
+ end
50
+ end
51
+
52
+ def merge_request_changed_files
53
+ @merge_request_changed_files ||= gitlab_merge_request.merge_request_changed_files
54
+ end
55
+
56
+ def slow_tests(test_results)
57
+ test_results.select(&:slow_test?)
58
+ end
59
+
60
+ def note_header
61
+ [
62
+ SLOW_TEST_MESSAGE,
63
+ SLOW_TEST_LABEL,
64
+ ':snail: Slow tests detected in this merge request, might be related with changed RSpec files.',
65
+ '| Job | File | Name | Duration | Expected duration |',
66
+ '| --- | --- | --- | --- | --- |'
67
+ ]
68
+ end
69
+
70
+ def slow_test_rows(slow_test)
71
+ rows = []
72
+
73
+ slow_test.each do |test|
74
+ rows << slow_test_table_row(test)
75
+ end
76
+
77
+ rows
78
+ end
79
+
80
+ def build_note(slow_test)
81
+ rows = note_header + slow_test_rows(slow_test)
82
+
83
+ rows.join("\n")
84
+ end
85
+
86
+ def note_comment_includes_slow_test?(gitlab_note, slow_test)
87
+ gitlab_note.include?("[##{slow_test.ci_job_id}](#{slow_test.ci_job_url}) | #{slow_test.test_file_link}")
88
+ end
89
+
90
+ def slow_test_table_row(slow_test)
91
+ row = [
92
+ "[##{slow_test.ci_job_id}](#{slow_test.ci_job_url})",
93
+ slow_test.test_file_link,
94
+ slow_test.name,
95
+ "#{slow_test.run_time} s",
96
+ "< #{slow_test.max_duration_for_test} s"
97
+ ]
98
+
99
+ "| #{row.join(' | ')} |"
100
+ end
101
+
102
+ def add_slow_test_rows(gitlab_note, slow_tests)
103
+ slow_tests.each do |slow_test|
104
+ gitlab_note += "\n#{slow_test_table_row(slow_test)}" unless note_comment_includes_slow_test?(gitlab_note, slow_test)
105
+ end
106
+
107
+ gitlab_note
108
+ end
109
+
110
+ def upsert_mr_note(slow_tests)
111
+ existing_note = gitlab_merge_request.find_note(body: SLOW_TEST_MESSAGE)
112
+
113
+ if existing_note
114
+ puts "Update note for merge request: #{merge_request_iid}"
115
+
116
+ up_to_date_note = add_slow_test_rows(existing_note.body, slow_tests)
117
+
118
+ gitlab_merge_request.update_note(id: existing_note['id'], note: up_to_date_note) if existing_note.body != up_to_date_note
119
+ else
120
+ up_to_date_note = build_note(slow_tests)
121
+
122
+ puts "Create note for merge request: #{merge_request_iid}"
123
+
124
+ gitlab_merge_request.create_note(note: up_to_date_note)
125
+ end
126
+ end
127
+
128
+ def validate_input!
129
+ assert_project!
130
+ assert_input_files!(files)
131
+ end
132
+
133
+ def assert_project!
134
+ return if project
135
+
136
+ abort "Please provide a valid project ID or path with the `-p/--project` option!"
137
+ end
138
+
139
+ def assert_input_files!(files)
140
+ return if Dir.glob(files).any?
141
+
142
+ abort "Please provide valid JUnit report files. No files were found matching `#{files.join(',')}`"
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -8,19 +8,6 @@ module GitlabQuality
8
8
  class ReportAsIssue
9
9
  include Concerns::Utils
10
10
 
11
- FILE_BASE_URL = "https://gitlab.com/gitlab-org/gitlab/-/blob/master/"
12
-
13
- OTHER_TESTS_MAX_DURATION = 45.40 # seconds
14
-
15
- TestLevelSpecification = Struct.new(:regex, :max_duration)
16
-
17
- TEST_LEVEL_SPECIFICATIONS = [
18
- TestLevelSpecification.new(%r{spec/features/}, 50.13),
19
- TestLevelSpecification.new(%r{spec/(controllers|requests)/}, 19.20),
20
- TestLevelSpecification.new(%r{spec/lib/}, 27.12),
21
- TestLevelSpecification.new(%r{qa/specs/features/}, 240)
22
- ].freeze
23
-
24
11
  def initialize(token:, input_files:, project: nil, dry_run: false, **_kwargs)
25
12
  @project = project
26
13
  @gitlab = (dry_run ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project)
@@ -51,22 +38,16 @@ module GitlabQuality
51
38
 
52
39
  | Field | Value |
53
40
  | ------ | ------ |
54
- | File | #{test_file_link(test)} |
41
+ | File | #{test.test_file_link} |
55
42
  | Description | `#{test.name}` |
56
43
  | Test level | #{test.level} |
57
44
  | Hash | `#{test_hash(test)}` |
58
45
  | Duration | #{test.run_time} seconds |
59
- | Expected duration | < #{max_duration_for_test(test)} seconds |
46
+ | Expected duration | < #{test.max_duration_for_test} seconds |
60
47
  #{"| Test case | #{test.testcase} |" if test.testcase}
61
48
  DESCRIPTION
62
49
  end
63
50
 
64
- def test_file_link(test)
65
- path_prefix = test.file.start_with?('qa/') ? 'qa/' : ''
66
-
67
- "[`#{path_prefix}#{test.file}#L#{test.line_number}`](#{FILE_BASE_URL}#{path_prefix}#{test.file}#L#{test.line_number})"
68
- end
69
-
70
51
  def new_issue_labels(_test)
71
52
  []
72
53
  end
@@ -169,15 +150,6 @@ module GitlabQuality
169
150
  def ee_test?(test)
170
151
  test.file =~ %r{features/ee/(api|browser_ui)}
171
152
  end
172
-
173
- def max_duration_for_test(test)
174
- test_level_specification = TEST_LEVEL_SPECIFICATIONS.find do |test_level_specification|
175
- test.example_id =~ test_level_specification.regex
176
- end
177
- return OTHER_TESTS_MAX_DURATION unless test_level_specification
178
-
179
- test_level_specification.max_duration
180
- end
181
153
  end
182
154
  end
183
155
  end
@@ -27,7 +27,7 @@ module GitlabQuality
27
27
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
28
28
 
29
29
  test_results.each do |test|
30
- create_slow_issue(test) if should_create_slow_issue?(test)
30
+ create_slow_issue(test) if test.slow_test?
31
31
  end
32
32
  end
33
33
  end
@@ -59,10 +59,6 @@ module GitlabQuality
59
59
  rescue MultipleIssuesFound => e
60
60
  warn(e.message)
61
61
  end
62
-
63
- def should_create_slow_issue?(test)
64
- !test.allowed_to_be_slow? && test.run_time > max_duration_for_test(test)
65
- end
66
62
  end
67
63
  end
68
64
  end
@@ -4,8 +4,21 @@ module GitlabQuality
4
4
  module TestTooling
5
5
  module TestResult
6
6
  class JsonTestResult < BaseTestResult
7
+ FILE_BASE_URL = "https://gitlab.com/gitlab-org/gitlab/-/blob/master/"
8
+
7
9
  PRIVATE_TOKEN_REGEX = /(private_token=)[\w-]+/
8
10
 
11
+ OTHER_TESTS_MAX_DURATION = 45.40 # seconds
12
+
13
+ TestLevelSpecification = Struct.new(:regex, :max_duration)
14
+
15
+ TEST_LEVEL_SPECIFICATIONS = [
16
+ TestLevelSpecification.new(%r{spec/features/}, 50.13),
17
+ TestLevelSpecification.new(%r{spec/(controllers|requests)/}, 19.20),
18
+ TestLevelSpecification.new(%r{spec/lib/}, 27.12),
19
+ TestLevelSpecification.new(%r{qa/specs/features/}, 240)
20
+ ].freeze
21
+
9
22
  def name
10
23
  report.fetch('full_description')
11
24
  end
@@ -93,6 +106,10 @@ module GitlabQuality
93
106
  report['level']
94
107
  end
95
108
 
109
+ def ci_job_id
110
+ report['ci_job_url'].split('/').last
111
+ end
112
+
96
113
  def failures # rubocop:disable Metrics/AbcSize
97
114
  @failures ||=
98
115
  report.fetch('exceptions', []).filter_map do |exception|
@@ -123,6 +140,25 @@ module GitlabQuality
123
140
  !!report['allowed_to_be_slow']
124
141
  end
125
142
 
143
+ def slow_test?
144
+ !allowed_to_be_slow? && run_time > max_duration_for_test
145
+ end
146
+
147
+ def max_duration_for_test
148
+ test_level_specification = TEST_LEVEL_SPECIFICATIONS.find do |test_level_specification|
149
+ example_id =~ test_level_specification.regex
150
+ end
151
+ return OTHER_TESTS_MAX_DURATION unless test_level_specification
152
+
153
+ test_level_specification.max_duration
154
+ end
155
+
156
+ def test_file_link
157
+ path_prefix = file.start_with?('qa/') ? 'qa/' : ''
158
+
159
+ "[`#{path_prefix}#{file}#L#{line_number}`](#{FILE_BASE_URL}#{path_prefix}#{file}#L#{line_number})"
160
+ end
161
+
126
162
  private
127
163
 
128
164
  def quarantine
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "0.9.3"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
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: 0.9.3
4
+ version: 1.0.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: 2023-07-26 00:00:00.000000000 Z
11
+ date: 2023-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -324,6 +324,7 @@ executables:
324
324
  - relate-failure-issue
325
325
  - report-results
326
326
  - slow-test-issues
327
+ - slow-test-merge-request-report-note
327
328
  - update-screenshot-paths
328
329
  extensions: []
329
330
  extra_rdoc_files: []
@@ -348,9 +349,12 @@ files:
348
349
  - exe/relate-failure-issue
349
350
  - exe/report-results
350
351
  - exe/slow-test-issues
352
+ - exe/slow-test-merge-request-report-note
351
353
  - exe/update-screenshot-paths
352
354
  - lefthook.yml
353
355
  - lib/gitlab_quality/test_tooling.rb
356
+ - lib/gitlab_quality/test_tooling/gitlab_client/merge_request.rb
357
+ - lib/gitlab_quality/test_tooling/gitlab_client/merge_request_dry_client.rb
354
358
  - lib/gitlab_quality/test_tooling/gitlab_issue_client.rb
355
359
  - lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb
356
360
  - lib/gitlab_quality/test_tooling/labels_inference.rb
@@ -359,6 +363,7 @@ files:
359
363
  - lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
360
364
  - lib/gitlab_quality/test_tooling/report/concerns/utils.rb
361
365
  - lib/gitlab_quality/test_tooling/report/generate_test_session.rb
366
+ - lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb
362
367
  - lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb
363
368
  - lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb
364
369
  - lib/gitlab_quality/test_tooling/report/report_as_issue.rb