# frozen_string_literal: true module GitlabQuality module TestTooling module Report # Uses the API to create GitLab issues for slow tests # # - Takes the JSON test reports like rspec-*.json` # - Takes a project where slow issues should be created # - Find issue by title (with test description or test file) # - Add test metadata, duration to the issue with group and category labels class SlowTestIssue < ReportAsIssue include Concerns::GroupAndCategoryLabels include Concerns::IssueReports IDENTITY_LABELS = ['test', 'rspec:slow test', 'rspec profiling', 'automation:bot-authored'].freeze NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3']).freeze SEARCH_LABELS = ['test'].freeze FOUND_IN_MR_LABEL = '~"found:in MR"' FOUND_IN_MASTER_LABEL = '~"found:master"' REPORT_SECTION_HEADER = '### Slowness reports' REPORTS_DOCUMENTATION = <<~DOC Slow tests were detected, please see the [test speed best practices guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed) to improve them. More context available about this issue in the [top slow tests guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#top-slow-tests). Add `allowed_to_be_slow: true` to the RSpec test if this is a legit slow test and close the issue. DOC private def run! puts "Reporting slow tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`." TestResults::Builder.new(files).test_results_per_file do |test_results| puts "=> Reporting #{test_results.count} tests in #{test_results.path}" process_test_results(test_results) end end def process_test_results(test_results) test_results.each do |test| next unless test.slow_test? puts " => Reporting slowness for test '#{test.name}'..." issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS) issues << create_issue(test) if issues.empty? update_reports(issues, test) collect_issues(test, issues) end end def test_is_applicable?(test) test.slow_test? end def update_reports(issues, test) issues.each do |issue| puts " => Adding the slow test to the existing issue: #{issue.web_url}" add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue])) end end def add_report_to_issue(issue:, test:, related_issues:) current_reports_note = existing_reports_note(issue: issue) new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test) note_body = [ new_reports_list.to_s, slowness_status_labels_quick_action(new_reports_list.reports_count), identity_labels_quick_action, relate_issues_quick_actions(related_issues) ].join("\n") if current_reports_note gitlab.edit_issue_note( issue_iid: issue.iid, note_id: current_reports_note.id, note: note_body ) else gitlab.create_issue_note(iid: issue.iid, note: note_body) end end def existing_reports_note(issue:) gitlab.find_issue_notes(iid: issue.iid).find do |note| note.body.start_with?(REPORT_SECTION_HEADER) end end def add_report_for_test(current_reports_content:, test:) increment_reports( current_reports_content: current_reports_content, test: test, reports_section_header: REPORT_SECTION_HEADER, item_extra_content: "(#{test.run_time} seconds) #{found_label}", reports_extra_content: REPORTS_DOCUMENTATION ) end def found_label if ENV.key?('CI_MERGE_REQUEST_IID') FOUND_IN_MR_LABEL else FOUND_IN_MASTER_LABEL end end # The report count is based on the percentiles of slowness issues. # # See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb # to gather these statistics. # # P75 => slowness::4 # P90 => slowness::3 # P95 => slowness::2 # Above P95 => slowness::1 def slowness_status_labels_quick_action(reports_count) case reports_count when 40..Float::INFINITY '/label ~"slowness::1"' when 28..39 '/label ~"slowness::2"' when 12..27 '/label ~"slowness::3"' else '/label ~"slowness::4"' end end def identity_labels_quick_action labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ') %(/label #{labels_list}) end def relate_issues_quick_actions(issues) issues.map do |issue| "/relate #{issue.web_url}" end.join("\n") end end end end end