gitlab_quality-test_tooling 1.12.0 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b88e790bd74462ff96779cb28d8f8f2cdadd99356e3889f50ec56ed7f5ea691d
4
- data.tar.gz: 865098baec02cb4f7cf6634fc7e7cb45a0394261957d9b0cdc33b9c0efd74c29
3
+ metadata.gz: 0b5576ca28a8e8cbf76564017a53418fd059747789bbbb8146835285d8f5b37c
4
+ data.tar.gz: b1d7c6dd581dccabe91f64687b420a83f5561aa407e1e4583e7a5146183890f4
5
5
  SHA512:
6
- metadata.gz: 1bdaa39d9a6e2ea9000d3318d54e1c3385c552fa967b86780f697ee10becf7e6f0cc3c8edf19a7ac7d29988babaf6ad735a1191d7132ed7f635939cde3575fa6
7
- data.tar.gz: 3edfd0209db270145f48338dbf0ebc1785176f8fd158fef8880b13b3280e8a569437d30f26234203bb58bbb6b111992265209882df4c783b83342b942d2d7d00
6
+ metadata.gz: d245b9be5a9a127630d934096277c339e649e9e719727f871d8b794245b385e6f4dbfba3175c546f44b2ee833099ef5398738dcaa631bab39b44eb9734fe338f
7
+ data.tar.gz: 33a391d891853d45e75198462d504641d090e0f619e31a9fbf5c613b4fe5cef6443d6b465e9e2ffc27d643a3d8d8426030bb10c872a83ca0a2def1b0232ae0ae
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (1.12.0)
4
+ gitlab_quality-test_tooling (1.13.0)
5
5
  activesupport (>= 6.1, < 7.2)
6
6
  amatch (~> 0.4.1)
7
7
  gitlab (~> 4.19)
data/README.md CHANGED
@@ -141,6 +141,21 @@ Usage: exe/slow-test-issues [options]
141
141
  -h, --help Show the usage
142
142
  ```
143
143
 
144
+ ### `exe/knapsack-report-issues`
145
+
146
+ ```shell
147
+ Purpose: Create spec run time issue when a spec file almost caused job timeout because it ran significantly longer than what Knapsack expected.
148
+ Usage: exe/knapsack-report-issues [options]
149
+ -i, --input-file INPUT_FILE Knapsack actual run time report file path glob
150
+ -e EXPECTED_REPORT, Knapsack expected report file path
151
+ --expected-report
152
+ -p, --project PROJECT Can be an integer or a group/project string
153
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
154
+ --dry-run Perform a dry-run (don't create issues)
155
+ -v, --version Show the version
156
+ -h, --help Show the usage
157
+ ```
158
+
144
159
  ### `exe/flaky-test-issues`
145
160
 
146
161
  ```shell
@@ -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-file INPUT_FILE", String, "Knapsack actual run time report file path glob") do |input_file|
15
+ params[:input_files] = input_file
16
+ end
17
+
18
+ opts.on("-e", "--expected-report EXPECTED_REPORT", String, "Knapsack expected report file path") do |report_path|
19
+ params[:expected_report] = report_path.strip
20
+ end
21
+
22
+ opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
23
+ params[:project] = project
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 issues)") 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 spec run time issue when a spec file almost caused job timeout because it ran significantly longer than what Knapsack expected."
42
+ puts opts
43
+ exit
44
+ end
45
+
46
+ opts.parse(ARGV)
47
+ end
48
+
49
+ if params.any?
50
+ GitlabQuality::TestTooling::Report::KnapsackReportIssue.new(**params).invoke!
51
+ else
52
+ puts options
53
+ exit 1
54
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module KnapsackReports
6
+ class SpecRunTime
7
+ attr_reader :file, :expected, :actual, :expected_suite_duration, :actual_suite_duration
8
+
9
+ ACTUAL_TO_EXPECTED_SPEC_RUN_TIME_RATIO_THRESHOLD = 1.5 # actual run time is longer than expected by 50% +
10
+ SPEC_WEIGHT_PERCENTAGE_TRESHOLD = 15 # a spec file takes 15%+ of the total test suite run time
11
+ SUITE_DURATION_THRESHOLD = 70 * 60 # if test suite takes more than 70 minutes, job risks timing out
12
+
13
+ def initialize(file:, expected:, actual:, expected_suite_duration:, actual_suite_duration:)
14
+ @file = file
15
+ @expected = expected.to_f
16
+ @actual = actual.to_f
17
+ @expected_suite_duration = expected_suite_duration.to_f
18
+ @actual_suite_duration = actual_suite_duration.to_f
19
+ end
20
+
21
+ def should_report?
22
+ # guideline proposed in https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/354
23
+ exceed_actual_to_expected_ratio_threshold? && test_suite_bottleneck?
24
+ end
25
+
26
+ def ci_pipeline_url_markdown
27
+ "[#{ci_pipeline_id}](#{ci_pipeline_url})"
28
+ end
29
+
30
+ def ci_pipeline_created_at
31
+ ENV.fetch('CI_PIPELINE_CREATED_AT', nil)
32
+ end
33
+
34
+ def ci_job_link_markdown
35
+ "[#{ci_job_name}](#{ci_job_url})"
36
+ end
37
+
38
+ def file_link_markdown
39
+ "[#{file}](#{file_link})"
40
+ end
41
+
42
+ def actual_percentage
43
+ (actual / actual_suite_duration * 100).round(2)
44
+ end
45
+
46
+ def name
47
+ nil
48
+ end
49
+
50
+ private
51
+
52
+ def exceed_actual_to_expected_ratio_threshold?
53
+ actual / expected >= ACTUAL_TO_EXPECTED_SPEC_RUN_TIME_RATIO_THRESHOLD
54
+ end
55
+
56
+ def test_suite_bottleneck?
57
+ # now we only report bottlenecks when they risk causing job timeouts
58
+ return unless actual_suite_duration > SUITE_DURATION_THRESHOLD
59
+
60
+ actual_percentage > SPEC_WEIGHT_PERCENTAGE_TRESHOLD
61
+ end
62
+
63
+ def ci_job_url
64
+ ENV.fetch('CI_JOB_URL', nil)
65
+ end
66
+
67
+ def ci_job_name
68
+ ENV.fetch('CI_JOB_NAME_SLUG', nil)
69
+ end
70
+
71
+ def ci_pipeline_id
72
+ ENV.fetch('CI_PIPELINE_IID', nil)
73
+ end
74
+
75
+ def ci_pipeline_url
76
+ ENV.fetch('CI_PIPELINE_URL', nil)
77
+ end
78
+
79
+ def file_link
80
+ "#{Runtime::Env.file_base_url}#{file}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module KnapsackReports
8
+ class SpecRunTimeReport
9
+ attr_reader :expected_report, :actual_report
10
+
11
+ def initialize(expected_report_path:, actual_report_path:)
12
+ @expected_report = parse(expected_report_path)
13
+ @actual_report = parse(actual_report_path)
14
+ end
15
+
16
+ def filtered_report
17
+ @filtered_report = actual_report.keys.filter_map do |spec_file|
18
+ expected_run_time = expected_report[spec_file]
19
+ actual_run_time = actual_report[spec_file]
20
+
21
+ if expected_run_time.nil?
22
+ puts "#{spec_file} missing from the expected Knapsack report, skipping."
23
+ next
24
+ end
25
+
26
+ spec_run_time = SpecRunTime.new(
27
+ file: spec_file,
28
+ expected: expected_run_time,
29
+ actual: actual_run_time,
30
+ expected_suite_duration: expected_test_suite_run_time_total,
31
+ actual_suite_duration: actual_test_suite_run_time_total
32
+ )
33
+
34
+ spec_run_time if spec_run_time.should_report?
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parse(report_path)
41
+ JSON.parse(File.read(report_path))
42
+ end
43
+
44
+ def expected_test_suite_run_time_total
45
+ @expected_test_suite_run_time_total ||=
46
+ expected_report.reduce(0) do |total_run_time, (_spec_file, run_time)|
47
+ total_run_time + run_time
48
+ end
49
+ end
50
+
51
+ def actual_test_suite_run_time_total
52
+ @actual_test_suite_run_time_total ||=
53
+ actual_report.reduce(0) do |total_run_time, (_spec_file, run_time)|
54
+ total_run_time + run_time
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -45,6 +45,28 @@ module GitlabQuality
45
45
 
46
46
  @pipeline ||= Runtime::Env.pipeline_from_project_name
47
47
  end
48
+
49
+ def readable_duration(duration_in_seconds)
50
+ minutes = (duration_in_seconds / 60).to_i
51
+ seconds = (duration_in_seconds % 60).round(2)
52
+
53
+ min_output = normalize_duration_output(minutes, 'minute')
54
+ sec_output = normalize_duration_output(seconds, 'second')
55
+
56
+ "#{min_output} #{sec_output}".strip
57
+ end
58
+
59
+ private
60
+
61
+ def normalize_duration_output(number, unit)
62
+ if number <= 0
63
+ ""
64
+ elsif number <= 1
65
+ "#{number} #{unit}"
66
+ else
67
+ "#{number} #{unit}s"
68
+ end
69
+ end
48
70
  end
49
71
  end
50
72
  end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Report
6
+ # Uses the API to create GitLab issues for spec run time exceeding Knapsack expectation
7
+ #
8
+ # - Takes the expected and actual Knapsack JSON reports from the knapsack output
9
+ # - Takes a project where issues should be created
10
+ # - For every test file reported with unexpectedly long run time:
11
+ # - Find issue by test file name, and if found:
12
+ # - Reopen issue if it already exists, but is closed
13
+ # - Update the issue with the new run time data
14
+ # - If not found:
15
+ # - Create a new issue with the run time data
16
+ class KnapsackReportIssue < ReportAsIssue
17
+ NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3', 'knapsack_report']).freeze
18
+ SEARCH_LABELS = %w[test maintenance::performance knapsack_report].freeze
19
+ JOB_TIMEOUT_EPIC_URL = 'https://gitlab.com/groups/gitlab-org/quality/engineering-productivity/-/epics/19'
20
+
21
+ def initialize(token:, input_files:, expected_report:, project: nil, dry_run: false)
22
+ super
23
+
24
+ @expected_report = expected_report
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :expected_report
30
+
31
+ def run!
32
+ puts "Reporting spec file exceeding Knapsack expectaton issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
33
+
34
+ search_and_create_issue
35
+ end
36
+
37
+ def search_and_create_issue
38
+ filtered_report = KnapsackReports::SpecRunTimeReport.new(
39
+ expected_report_path: expected_report_path,
40
+ actual_report_path: actual_report_path
41
+ ).filtered_report
42
+
43
+ puts "=> Reporting #{filtered_report.count} spec files exceeding Knapsack expectation."
44
+
45
+ filtered_report.each do |spec_with_run_time|
46
+ existing_issues = find_issues_for_test(spec_with_run_time, labels: SEARCH_LABELS)
47
+
48
+ if existing_issues.empty?
49
+ puts "Creating issue for #{spec_with_run_time.file}"
50
+ create_issue(spec_with_run_time)
51
+ else
52
+ update_issue(issue: existing_issues.last, spec_run_time: spec_with_run_time)
53
+ end
54
+ end
55
+ end
56
+
57
+ def expected_report_path
58
+ return if expected_report.nil? || !File.exist?(expected_report)
59
+
60
+ expected_report
61
+ end
62
+
63
+ def actual_report_path
64
+ return if files.nil? || !File.exist?(files.first)
65
+
66
+ files.first
67
+ end
68
+
69
+ def new_issue_labels(_spec_run_time)
70
+ NEW_ISSUE_LABELS
71
+ end
72
+
73
+ def new_issue_title(spec_run_time)
74
+ "Job timeout risk: #{spec_run_time.file} ran much longer than expected"
75
+ end
76
+
77
+ def new_issue_description(spec_run_time)
78
+ <<~MARKDOWN.chomp
79
+ /epic #{JOB_TIMEOUT_EPIC_URL}
80
+
81
+ ### Why was this issue created?
82
+
83
+ #{spec_run_time.file_link_markdown} was reported to have:
84
+
85
+ 1. exceeded Knapsack's expected runtime by at least 50%, and
86
+ 2. been identified as a notable pipeline bottleneck and a job timeout risk
87
+
88
+ ### Suggested steps for investigation
89
+
90
+ 1. To reproduce in CI by running test files in the same order, you can follow the steps listed [here](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html#recreate-job-failure-in-ci-by-forcing-the-job-to-run-the-same-set-of-test-files).
91
+ 1. Identify if a specific test case is stalling the run time. Hint: You can search the job's log for `Starting example group #{spec_run_time.file}` and view the elapsed time after each test case in the proceeding lines starting with `[RSpecRunTime]`.
92
+ 1. If the test file is large, consider refactoring it into multiple files to allow better test parallelization across runners.
93
+ 1. If the run time cannot be fixed in time, consider quarantine the spec(s) to restore performance.
94
+
95
+ ### Run time details
96
+
97
+ #{run_time_detail(spec_run_time)}
98
+ MARKDOWN
99
+ end
100
+
101
+ def update_issue(issue:, spec_run_time:)
102
+ state_event = issue.state == 'closed' ? 'reopen' : nil
103
+ updated_description = <<~MARKDOWN.chomp
104
+ #{issue.description}
105
+
106
+ #{run_time_detail(spec_run_time)}
107
+ MARKDOWN
108
+
109
+ issue_attrs = {
110
+ description: updated_description
111
+ }
112
+
113
+ issue_attrs[:state_event] = state_event if state_event
114
+
115
+ gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
116
+ puts " => Added a report in #{issue.web_url}!"
117
+ end
118
+
119
+ def run_time_detail(spec_run_time)
120
+ <<~MARKDOWN.chomp
121
+ - Reported from pipeline #{spec_run_time.ci_pipeline_url_markdown} created at `#{spec_run_time.ci_pipeline_created_at}`
122
+
123
+ | Field | Value |
124
+ | ------ | ------ |
125
+ | Job URL| #{spec_run_time.ci_job_link_markdown} |
126
+ | Job total RSpec suite run time | expected: `#{readable_duration(spec_run_time.expected_suite_duration)}`, actual: `#{readable_duration(spec_run_time.actual_suite_duration)}` |
127
+ | Spec file run time | expected: `#{readable_duration(spec_run_time.expected)}`, actual: `#{readable_duration(spec_run_time.actual)}` |
128
+ | Spec file weight | `#{spec_run_time.actual_percentage}%` of total suite run time |
129
+ MARKDOWN
130
+ end
131
+
132
+ def assert_input_files!(_files)
133
+ missing_expected_report_msg = "Missing a valid expected Knapsack report."
134
+ missing_actual_report_msg = "Missing a valid actual Knapsack report."
135
+
136
+ abort missing_expected_report_msg if expected_report_path.nil?
137
+ abort missing_actual_report_msg if actual_report_path.nil?
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -64,8 +64,6 @@ module GitlabQuality
64
64
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
65
65
  process_test_results(test_results)
66
66
  end
67
-
68
- write_issues_log_file
69
67
  end
70
68
 
71
69
  def test_metric_collections
@@ -19,7 +19,11 @@ module GitlabQuality
19
19
  def invoke!
20
20
  validate_input!
21
21
 
22
- run!
22
+ issue_url = run!
23
+
24
+ write_issues_log_file
25
+
26
+ issue_url
23
27
  end
24
28
 
25
29
  private
@@ -162,7 +166,12 @@ module GitlabQuality
162
166
  def issue_match_test?(issue, test)
163
167
  issue_title = issue.title.strip
164
168
  test_file_path_found = !test.file.to_s.empty? && issue_title.include?(partial_file_path(test.file))
165
- issue_title.include?(test.name) || test_file_path_found
169
+
170
+ if test.name
171
+ issue_title.include?(test.name) || test_file_path_found
172
+ else
173
+ test_file_path_found
174
+ end
166
175
  end
167
176
 
168
177
  def pipeline_name_label
@@ -33,8 +33,6 @@ module GitlabQuality
33
33
  create_slow_issue(test) if test.slow_test?
34
34
  end
35
35
  end
36
-
37
- write_issues_log_file
38
36
  end
39
37
 
40
38
  def new_issue_title(test)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.12.0"
5
+ VERSION = "1.13.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: 1.12.0
4
+ version: 1.13.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-01-11 00:00:00.000000000 Z
11
+ date: 2024-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -348,6 +348,7 @@ email:
348
348
  executables:
349
349
  - flaky-test-issues
350
350
  - generate-test-session
351
+ - knapsack-report-issues
351
352
  - post-to-slack
352
353
  - prepare-stage-reports
353
354
  - relate-failure-issue
@@ -374,6 +375,7 @@ files:
374
375
  - Rakefile
375
376
  - exe/flaky-test-issues
376
377
  - exe/generate-test-session
378
+ - exe/knapsack-report-issues
377
379
  - exe/post-to-slack
378
380
  - exe/prepare-stage-reports
379
381
  - exe/relate-failure-issue
@@ -396,6 +398,8 @@ files:
396
398
  - lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb
397
399
  - lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb
398
400
  - lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb
401
+ - lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time.rb
402
+ - lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time_report.rb
399
403
  - lib/gitlab_quality/test_tooling/labels_inference.rb
400
404
  - lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
401
405
  - lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
@@ -404,6 +408,7 @@ files:
404
408
  - lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
405
409
  - lib/gitlab_quality/test_tooling/report/generate_test_session.rb
406
410
  - lib/gitlab_quality/test_tooling/report/issue_logger.rb
411
+ - lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb
407
412
  - lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb
408
413
  - lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb
409
414
  - lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb