class Gitlab::QA::Report::RelateFailureIssue
Uses the API to create or update GitLab issues with the results of tests from RSpec report files.
def failure_issues(test)
def failure_issues(test) gitlab.find_issues(options: { state: 'opened', labels: 'QA' }).select do |issue| issue_title = issue.title.strip issue_title.include?(test.name) || issue_title.include?(partial_file_path(test.file)) end end
def find_failure_issue(test)
def find_failure_issue(test) relevant_issues = find_relevant_failure_issues(test) return nil if relevant_issues.empty? best_matching_issue, smaller_diff_ratio = relevant_issues.min_by { |_, diff_ratio| diff_ratio } unless relevant_issues.values.count(smaller_diff_ratio) == 1 # rubocop:disable Style/IfUnlessModifier raise(MultipleIssuesFound, %(Too many issues found for test '#{test.name}' (`#{test.file}`)!)) end # Re-instantiate a `Gitlab::ObjectifiedHash` object after having converted it to a hash in #find_relevant_failure_issues above. best_matching_issue = Gitlab::ObjectifiedHash.new(best_matching_issue) test.failure_issue ||= best_matching_issue.web_url [best_matching_issue, smaller_diff_ratio] end
def find_issue_stacktrace(issue)
def find_issue_stacktrace(issue) issue_stacktrace_match = issue.description.match(STACKTRACE_REGEX) if issue_stacktrace_match issue_stacktrace_match[2].gsub(/^#.*$/, '').strip else puts " => [DEBUG] Stacktrace couldn't be found for #{issue.web_url}:\n\n#{issue.description}\n\n----------------------------------\n" end end
def find_or_create_issue(test)
def find_or_create_issue(test) issue, diff_ratio = find_failure_issue(test) if issue puts " => Found issue #{issue.web_url} for test '#{test.name}' with a diff ratio of #{(diff_ratio * 100).round(2)}%." else issue = create_issue(test) puts " => Created new issue: #{issue.web_url} for test '#{test.name}'." if issue end issue end
def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize ld = Class.new.extend(Gem::Text).method(:levenshtein_distance) first_test_failure_stacktrace = test.failures.first['message_lines'].join("\n") # Search with the `search` param returns 500 errors, so we filter by ~QA and then filter further in Ruby failure_issues(test).each_with_object({}) do |issue, memo| relevant_issue_stacktrace = find_issue_stacktrace(issue) next unless relevant_issue_stacktrace distance = ld.call(first_test_failure_stacktrace, relevant_issue_stacktrace) diff_ratio = distance.zero? ? 0.0 : (distance.to_f / first_test_failure_stacktrace.size).round(3) if diff_ratio <= max_diff_ratio puts " => [DEBUG] Issue #{issue} has an acceptable diff ratio of #{(diff_ratio * 100).round(2)}%." # The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key. # This leads to a `TypeError Exception: no implicit conversion of Hash into Integer` error, so we convert the object to a hash before using it as a Hash key. # See: # - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995 # - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494 memo[issue.to_h] = diff_ratio else puts " => [DEBUG] Found issue #{issue.web_url} but stacktraces are too different (#{(diff_ratio * 100).round(2)}%)." end end end
def initialize(max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, **kwargs)
def initialize(max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, **kwargs) super @max_diff_ratio = max_diff_ratio.to_f end
def new_issue_description(test)
def new_issue_description(test) super + [ "\n\n### Stack trace", "```\n#{test.failures.first['message_lines'].join("\n")}\n```", "First happened in #{test.ci_job_url}." ].join("\n\n") end
def new_issue_labels(test)
def new_issue_labels(test) NEW_ISSUE_LABELS + up_to_date_labels(test: test) end
def new_issue_title(test)
def new_issue_title(test) "Failure in #{super}" end
def post_failed_job_note(issue, test)
def post_failed_job_note(issue, test) gitlab.create_issue_note(iid: issue.iid, note: "/relate #{test.testcase}") end
def relate_test_to_issue(test)
def relate_test_to_issue(test) puts " => Searching issues for test '#{test.name}'..." begin issue = find_or_create_issue(test) return unless issue update_labels(issue, test) post_failed_job_note(issue, test) puts " => Marked #{issue.web_url} as related to #{test.testcase}." rescue MultipleIssuesFound => e warn(e.message) end end
def run!
def run! puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`." test_results_per_file do |test_results| puts "=> Reporting tests in #{test_results.path}" test_results.each do |test| next if test.failures.empty? relate_test_to_issue(test) end test_results.write end end
def up_to_date_labels(test:, issue: nil)
def up_to_date_labels(test:, issue: nil) super << pipeline_name_label end