From 87f9cb8c0e278253af9b49a939c67a5dca732abf Mon Sep 17 00:00:00 2001
From: Mark Fletcher <mark@gitlab.com>
Date: Wed, 3 Nov 2021 20:03:39 +0000
Subject: [PATCH] Make previous test detection work for other projects and
 hosts

---
 .gitlab/ci/rules.gitlab-ci.yml                |   4 -
 scripts/pipeline_test_report_builder.rb       |  42 ++---
 scripts/rspec_helpers.sh                      |   7 +-
 .../pipeline_test_report_builder_spec.rb      | 174 +++++++++++-------
 4 files changed, 137 insertions(+), 90 deletions(-)

diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 8a782ec2b6844..fc68b8aa35475 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1263,16 +1263,12 @@
 
 .rails:rules:detect-previous-failed-tests:
   rules:
-    - <<: *if-not-canonical-namespace
-      when: never
     - <<: *if-merge-request-labels-run-all-rspec
     - <<: *if-merge-request
       changes: *code-backstage-patterns
 
 .rails:rules:rerun-previous-failed-tests:
   rules:
-    - <<: *if-not-canonical-namespace
-      when: never
     - <<: *if-merge-request-labels-run-all-rspec
     - <<: *if-merge-request
       changes: *code-backstage-patterns
diff --git a/scripts/pipeline_test_report_builder.rb b/scripts/pipeline_test_report_builder.rb
index 56491d40a3ea2..2101decf59ac2 100755
--- a/scripts/pipeline_test_report_builder.rb
+++ b/scripts/pipeline_test_report_builder.rb
@@ -20,7 +20,7 @@
 # Push into expected format for failed tests
 class PipelineTestReportBuilder
   def initialize(options)
-    @project = options.delete(:project)
+    @target_project = options.delete(:target_project)
     @mr_id = options.delete(:mr_id) || Host::DEFAULT_OPTIONS[:mr_id]
     @instance_base_url = options.delete(:instance_base_url) || Host::DEFAULT_OPTIONS[:instance_base_url]
     @output_file_path = options.delete(:output_file_path)
@@ -40,38 +40,38 @@ def execute
     end
   end
 
-  private
+  def previous_pipeline
+    # Top of the list will always be the current pipeline
+    # Second from top will be the previous pipeline
+    pipelines_for_mr.sort_by { |a| -Time.parse(a['created_at']).to_i }[1]
+  end
 
-  attr_reader :project, :mr_id, :instance_base_url, :output_file_path
+  private
 
-  def project_api_base_url
-    "#{instance_base_url}/api/v4/projects/#{CGI.escape(project)}"
-  end
+  attr_reader :target_project, :mr_id, :instance_base_url, :output_file_path
 
-  def project_base_url
-    "#{instance_base_url}/#{project}"
+  def pipeline_project_api_base_url(pipeline)
+    "#{instance_base_url}/api/v4/projects/#{pipeline['project_id']}"
   end
 
-  def previous_pipeline
-    # Top of the list will always be the current pipeline
-    # Second from top will be the previous pipeline
-    pipelines_for_mr.sort_by { |a| -Time.parse(a['created_at']).to_i }[1]
+  def target_project_api_base_url
+    "#{instance_base_url}/api/v4/projects/#{CGI.escape(target_project)}"
   end
 
   def pipelines_for_mr
-    fetch("#{project_api_base_url}/merge_requests/#{mr_id}/pipelines")
+    fetch("#{target_project_api_base_url}/merge_requests/#{mr_id}/pipelines")
   end
 
-  def failed_builds_for_pipeline(pipeline_id)
-    fetch("#{project_api_base_url}/pipelines/#{pipeline_id}/jobs?scope=failed&per_page=100")
+  def failed_builds_for_pipeline(pipeline)
+    fetch("#{pipeline_project_api_base_url(pipeline)}/pipelines/#{pipeline['id']}/jobs?scope=failed&per_page=100")
   end
 
   # Method uses the test suite endpoint to gather test results for a particular build.
   # Here we request individual builds, even though it is possible to supply multiple build IDs.
   # The reason for this; it is possible to lose the job context and name when requesting multiple builds.
   # Please see for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69053#note_709939709
-  def test_report_for_build(pipeline_id, build_id)
-    fetch("#{project_base_url}/-/pipelines/#{pipeline_id}/tests/suite.json?build_ids[]=#{build_id}")
+  def test_report_for_build(pipeline, build_id)
+    fetch("#{pipeline['web_url']}/tests/suite.json?build_ids[]=#{build_id}")
   end
 
   def build_test_report_json_for_pipeline(pipeline)
@@ -82,7 +82,7 @@ def build_test_report_json_for_pipeline(pipeline)
 
     puts "Discovered last failed pipeline (#{pipeline['id']}) for MR!#{mr_id}"
 
-    failed_builds_for_test_stage = failed_builds_for_pipeline(pipeline['id']).select do |failed_build|
+    failed_builds_for_test_stage = failed_builds_for_pipeline(pipeline).select do |failed_build|
       failed_build['stage'] == 'test'
     end
 
@@ -92,7 +92,7 @@ def build_test_report_json_for_pipeline(pipeline)
       test_report['suites'] ||= []
 
       failed_builds_for_test_stage.each do |failed_build|
-        test_report['suites'] << test_report_for_build(pipeline['id'], failed_build['id'])
+        test_report['suites'] << test_report_for_build(pipeline, failed_build['id'])
       end
     end
 
@@ -127,8 +127,8 @@ def fetch(uri_str)
   options = Host::DEFAULT_OPTIONS.dup
 
   OptionParser.new do |opts|
-    opts.on("-p", "--project PROJECT", String, "Project where to find the merge request(defaults to $CI_PROJECT_ID)") do |value|
-      options[:project] = value
+    opts.on("-t", "--target-project TARGET_PROJECT", String, "Project where to find the merge request") do |value|
+      options[:target_project] = value
     end
 
     opts.on("-m", "--mr-id MR_ID", String, "A merge request ID") do |value|
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 7ac6b98938fd0..a3565db58fabf 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -94,11 +94,14 @@ function retrieve_previous_failed_tests() {
   local rspec_pg_regex="${2}"
   local rspec_ee_pg_regex="${3}"
   local pipeline_report_path="test_results/previous/test_reports.json"
-  local project_path="gitlab-org/gitlab"
+
+  # Used to query merge requests. This variable reflects where the merge request has been created
+  local target_project_path="${CI_MERGE_REQUEST_PROJECT_PATH}"
+  local instance_url="${CI_SERVER_URL}"
 
   echo 'Attempting to build pipeline test report...'
 
-  scripts/pipeline_test_report_builder.rb --instance-base-url "https://gitlab.com" --project "${project_path}" --mr-id "${CI_MERGE_REQUEST_IID}" --output-file-path "${pipeline_report_path}"
+  scripts/pipeline_test_report_builder.rb --instance-base-url "${instance_url}" --target-project "${target_project_path}" --mr-id "${CI_MERGE_REQUEST_IID}" --output-file-path "${pipeline_report_path}"
 
   echo 'Generating failed tests lists...'
 
diff --git a/spec/scripts/pipeline_test_report_builder_spec.rb b/spec/scripts/pipeline_test_report_builder_spec.rb
index 8a5388f4db8a0..8553ada044ef1 100644
--- a/spec/scripts/pipeline_test_report_builder_spec.rb
+++ b/spec/scripts/pipeline_test_report_builder_spec.rb
@@ -9,30 +9,39 @@
 
   subject do
     described_class.new(
-      project: 'gitlab-org/gitlab',
+      target_project: 'gitlab-org/gitlab',
       mr_id: '999',
       instance_base_url: 'https://gitlab.com',
       output_file_path: output_file_path
     )
   end
 
-  let(:mr_pipelines) do
-    [
-      {
-        'status' => 'running',
-        'created_at' => DateTime.now.to_s
-      },
-      {
-        'status' => 'failed',
-        'created_at' => (DateTime.now - 5).to_s
-      }
-    ]
+  let(:failed_pipeline_url) { 'pipeline2_url' }
+
+  let(:failed_pipeline) do
+    {
+      'status' => 'failed',
+      'created_at' => (DateTime.now - 5).to_s,
+      'web_url' => failed_pipeline_url
+    }
   end
 
+  let(:current_pipeline) do
+    {
+      'status' => 'running',
+      'created_at' => DateTime.now.to_s,
+      'web_url' => 'pipeline1_url'
+    }
+  end
+
+  let(:mr_pipelines) { [current_pipeline, failed_pipeline] }
+
+  let(:failed_build_id) { 9999 }
+
   let(:failed_builds_for_pipeline) do
     [
       {
-        'id' => 9999,
+        'id' => failed_build_id,
         'stage' => 'test'
       }
     ]
@@ -62,75 +71,114 @@
   before do
     allow(subject).to receive(:pipelines_for_mr).and_return(mr_pipelines)
     allow(subject).to receive(:failed_builds_for_pipeline).and_return(failed_builds_for_pipeline)
-    allow(subject).to receive(:test_report_for_build).and_return(test_report_for_build)
   end
 
-  describe '#test_report_for_latest_pipeline' do
-    context 'no previous pipeline' do
-      let(:mr_pipelines) { [] }
+  describe '#previous_pipeline' do
+    let(:fork_pipeline_url) { 'fork_pipeline_url' }
+    let(:fork_pipeline) do
+      {
+        'status' => 'failed',
+        'created_at' => (DateTime.now - 5).to_s,
+        'web_url' => fork_pipeline_url
+      }
+    end
 
-      it 'returns empty hash' do
-        expect(subject.test_report_for_latest_pipeline).to eq("{}")
-      end
+    before do
+      allow(subject).to receive(:test_report_for_build).and_return(test_report_for_build)
     end
 
-    context 'first pipeline scenario' do
-      let(:mr_pipelines) do
-        [
-          {
-            'status' => 'running',
-            'created_at' => DateTime.now.to_s
-          }
-        ]
+    context 'pipeline in a fork project' do
+      let(:mr_pipelines) { [current_pipeline, fork_pipeline] }
+
+      it 'returns fork pipeline' do
+        expect(subject.previous_pipeline).to eq(fork_pipeline)
       end
+    end
 
-      it 'returns empty hash' do
-        expect(subject.test_report_for_latest_pipeline).to eq("{}")
+    context 'pipeline in target project' do
+      it 'returns failed pipeline' do
+        expect(subject.previous_pipeline).to eq(failed_pipeline)
       end
     end
+  end
 
-    context 'no previous failed pipeline' do
-      let(:mr_pipelines) do
-        [
-          {
-            'status' => 'running',
-            'created_at' => DateTime.now.to_s
-          },
-          {
-            'status' => 'success',
-            'created_at' => (DateTime.now - 5).to_s
-          }
-        ]
+  describe '#test_report_for_latest_pipeline' do
+    it 'fetches builds from pipeline related to MR' do
+      expect(subject).to receive(:fetch).with("#{failed_pipeline_url}/tests/suite.json?build_ids[]=#{failed_build_id}").and_return(failed_builds_for_pipeline)
+      subject.test_report_for_latest_pipeline
+    end
+
+    context 'canonical pipeline' do
+      before do
+        allow(subject).to receive(:test_report_for_build).and_return(test_report_for_build)
       end
 
-      it 'returns empty hash' do
-        expect(subject.test_report_for_latest_pipeline).to eq("{}")
+      context 'no previous pipeline' do
+        let(:mr_pipelines) { [] }
+
+        it 'returns empty hash' do
+          expect(subject.test_report_for_latest_pipeline).to eq("{}")
+        end
       end
-    end
 
-    context 'no failed test builds' do
-      let(:failed_builds_for_pipeline) do
-        [
-          {
-            'id' => 9999,
-            'stage' => 'prepare'
-          }
-        ]
+      context 'first pipeline scenario' do
+        let(:mr_pipelines) do
+          [
+            {
+              'status' => 'running',
+              'created_at' => DateTime.now.to_s
+            }
+          ]
+        end
+
+        it 'returns empty hash' do
+          expect(subject.test_report_for_latest_pipeline).to eq("{}")
+        end
       end
 
-      it 'returns empty hash' do
-        expect(subject.test_report_for_latest_pipeline).to eq("{}")
+      context 'no previous failed pipeline' do
+        let(:mr_pipelines) do
+          [
+            {
+              'status' => 'running',
+              'created_at' => DateTime.now.to_s
+            },
+            {
+              'status' => 'success',
+              'created_at' => (DateTime.now - 5).to_s
+            }
+          ]
+        end
+
+        it 'returns empty hash' do
+          expect(subject.test_report_for_latest_pipeline).to eq("{}")
+        end
+      end
+
+      context 'no failed test builds' do
+        let(:failed_builds_for_pipeline) do
+          [
+            {
+              'id' => 9999,
+              'stage' => 'prepare'
+            }
+          ]
+        end
+
+        it 'returns empty hash' do
+          expect(subject.test_report_for_latest_pipeline).to eq("{}")
+        end
       end
-    end
 
-    context 'failed pipeline and failed test builds' do
-      it 'returns populated test list for suites' do
-        actual = subject.test_report_for_latest_pipeline
-        expected = {
-          'suites' => [test_report_for_build]
-        }.to_json
+      context 'failed pipeline and failed test builds' do
+        it 'returns populated test list for suites' do
+          actual = subject.test_report_for_latest_pipeline
+          expected = {
+            'suites' => [test_report_for_build]
+          }.to_json
 
-        expect(actual).to eq(expected)
+          expect(actual).to eq(expected)
+        end
       end
     end
   end
-- 
GitLab