# frozen_string_literal: truerequire'nokogiri'require'active_support/core_ext/enumerable'moduleGitlabmoduleQAmoduleReport# Uses the API to create or update GitLab issues with the results of tests from RSpec report files.classResultsInIssues<ReportAsIssueprivateRESULTS_SECTION_TEMPLATE="\n\n### DO NOT EDIT BELOW THIS LINE\n\nActive and historical test results:"defrun!puts"Reporting test results in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."test_results_per_filedo|test_results|puts"Reporting tests in #{test_results.path}"test_results.eachdo|test|puts"Reporting test: #{test.file} | #{test.name}\n"report_test(test)unlesstest.skippedendtest_results.writeendenddefreport_test(test)testcase=find_testcase(test)||create_testcase(test)test.testcase||=testcase.web_url.sub('/issues/','/quality/test_cases/')issue=find_linked_results_issue_by_iid(testcase,test)ifissueissue=update_issue_title(issue,test,'issue')ifissue.title.strip!=title_from_test(test)elseputs"No valid issue link found"issue=find_or_create_results_issue(test)add_issue_to_testcase(testcase,issue)endupdate_labels(testcase,test)update_issue(issue,test)enddeffind_testcase(test)testcase=find_testcase_by_iid(test)iftestcasetestcase=update_issue_title(testcase,test,'test_case')iftestcase.title.strip!=title_from_test(test)elsetestcase=find_issue(test,'test_case')endtestcaseenddeffind_testcase_by_iid(test)iid=testcase_iid_from_url(test.testcase)returnunlessiidfind_issue_by_iid(iid,'test_case')enddeffind_linked_results_issue_by_iid(testcase,test)iid=issue_iid_from_testcase(testcase)returnunlessiidfind_issue_by_iid(iid,'issue')enddeffind_issue_by_iid(iid,issue_type)issues=gitlab.find_issues(iid: iid)do|issue|issue.state=='opened'&&issue.issue_type==issue_typeendwarn(%(#{issue_type} iid "#{iid}" not valid))ifissues.empty?issues.firstenddefupdate_issue_title(issue,test,issue_type)warn(%(#{issue_type} title needs to be updated from '#{issue.title.strip}' to '#{title_from_test(test)}'))gitlab.edit_issue(iid: issue.iid,options: {title: title_from_test(test)})enddefcreate_testcase(test)title=title_from_test(test)puts"Creating test case '#{title}' ..."gitlab.create_issue(title: title,description: new_testcase_description(test),labels: new_issue_labels(test),issue_type: 'test_case')enddeftestcase_iid_from_url(url)returnwarn(%(\nPlease update #{url} to test case url"))ifurl&.include?('/-/issues/')url&&url.split('/').last.to_ienddefnew_testcase_description(test)"#{new_issue_description(test)}#{RESULTS_SECTION_TEMPLATE}"enddefissue_iid_from_testcase(testcase)results=testcase.description.partition(RESULTS_SECTION_TEMPLATE).lastiftestcase.description.include?(RESULTS_SECTION_TEMPLATE)returnputs"No issue link found"unlessresultsissue_iid=results.split('/').lastissue_iid&.to_ienddeffind_or_create_results_issue(test)issue=find_issue(test,'issue')ifissueputs"Found existing issue: #{issue.web_url}"elseissue=create_issue(test)puts"Created new issue: #{issue.web_url}"endissueenddeffind_issue(test,issue_type)issues=gitlab.find_issues(options: {search: search_term(test)})do|issue|issue.state=='opened'&&issue.issue_type==issue_type&&issue.title.strip==title_from_test(test)endwarn(%(Too many #{issue_type}s found with the file path "#{test.file}" and name "#{test.name}"))ifissues.many?issues.firstenddefadd_issue_to_testcase(testcase,issue)results_section=testcase.description.include?(RESULTS_SECTION_TEMPLATE)?'':RESULTS_SECTION_TEMPLATEgitlab.edit_issue(iid: testcase.iid,options: {description: (testcase.description+results_section+"\n\n#{issue.web_url}")})puts"Added issue #{issue.web_url} to testcase #{testcase.web_url}"enddefupdate_issue(issue,test)new_labels=issue_labels(issue)new_labels|=['Testcase Linked']labels_updated=update_labels(issue,test,new_labels)note_posted=note_status(issue,test)iflabels_updated||note_postedputs"Issue updated."elseputs"Test passed, no update needed."endenddefnew_issue_labels(test)['Quality',"devops::#{test.stage}",'status::automated']enddefup_to_date_labels(test:,issue: nil,new_labels: Set.new)labels=superlabels|=new_issue_labels(test).to_setlabels.delete_if{|label|label.start_with?("#{pipeline}::")}labels<<(test.failures.empty??"#{pipeline}::passed":"#{pipeline}::failed")enddefsearch_term(test)%("#{partial_file_path(test.file)}" "#{search_safe(test.name)}")enddefnote_status(issue,test)returnfalseiftest.skippedreturnfalseiftest.failures.empty?note=note_content(test)gitlab.find_issue_discussions(iid: issue.iid).eachdo|discussion|returngitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid,discussion_id: discussion.id,body: failure_summary)ifnew_note_matches_discussion?(note,discussion)endgitlab.create_issue_note(iid: issue.iid,note: note)trueenddefnote_content(test)errors=test.failures.each_with_object([])do|failure,text|text<<<<~TEXT
Error:
```
#{failure['message']}
```
Stacktrace:
```
#{failure['stacktrace']}
```
TEXTend.join("\n\n")"#{failure_summary}\n\n#{errors}"enddeffailure_summarysummary=[":x: ~\"#{pipeline}::failed\""]summary<<"~\"quarantine\""ifquarantine_job?summary<<"in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}"summary.join(' ')enddefnew_note_matches_discussion?(note,discussion)note_error=error_and_stack_trace(note)discussion_error=error_and_stack_trace(discussion.notes.first['body'])returnfalseifnote_error.empty?||discussion_error.empty?note_error==discussion_errorenddeferror_and_stack_trace(text)text.strip[/Error:(.*)/m,1].to_sendendendendend