# frozen_string_literal: true require 'nokogiri' require 'active_support/core_ext/enumerable' module Gitlab module QA module Report # Uses the API to create or update GitLab issues with the results of tests from RSpec report files. class ResultsInIssues < ReportAsIssue private def run! puts "Reporting test results 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| report_test(test) end test_results.write end end def report_test(test) puts "Reporting test: #{test.file} | #{test.name}" issue = find_issue(test) if issue puts "Found existing issue: #{issue.web_url}" else # Don't create new issues for skipped tests return if test.skipped issue = create_issue(test) puts "Created new issue: #{issue.web_url}" end test.testcase ||= issue.web_url labels_updated = update_labels(issue, test) note_posted = note_status(issue, test) if labels_updated || note_posted puts "Issue updated." else puts "Test passed, no update needed." end end def find_issue(test) title = title_from_test(test) issues = gitlab.find_issues( iid: iid_from_testcase_url(test.testcase), options: { search: search_term(test) }) do |issue| issue.state == 'opened' && issue.title.strip == title end warn(%(Too many issues found with the file path "#{test.file}" and name "#{test.name}")) if issues.many? issues.first end def new_issue_labels(test) %w[status::automated] end def up_to_date_labels(test:, issue: nil) labels = super labels.delete_if { |label| label.start_with?("#{pipeline}::") } labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed") end def iid_from_testcase_url(url) url && url.split('/').last.to_i end def search_term(test) %("#{partial_file_path(test.file)}" "#{search_safe(test.name)}") end def note_status(issue, test) return false if test.skipped return false if test.failures.empty? note = note_content(test) gitlab.find_issue_discussions(iid: issue.iid).each do |discussion| return gitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid, discussion_id: discussion.id, body: failure_summary) if new_note_matches_discussion?(note, discussion) end gitlab.create_issue_note(iid: issue.iid, note: note) true end def note_content(test) errors = test.failures.each_with_object([]) do |failure, text| text << <<~TEXT Error: ``` #{failure['message']} ``` Stacktrace: ``` #{failure['stacktrace']} ``` TEXT end.join("\n\n") "#{failure_summary}\n\n#{errors}" end def failure_summary summary = [":x: ~\"#{pipeline}::failed\""] summary << "~\"quarantine\"" if quarantine_job? summary << "in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}" summary.join(' ') end def new_note_matches_discussion?(note, discussion) note_error = error_and_stack_trace(note) discussion_error = error_and_stack_trace(discussion.notes.first['body']) return false if note_error.empty? || discussion_error.empty? note_error == discussion_error end def error_and_stack_trace(text) text.strip[/Error:(.*)/m, 1].to_s end end end end end