diff --git a/dev/archery/archery/crossbow/reports.py b/dev/archery/archery/crossbow/reports.py index 1c6510ea4f35..0caa12bf4fe4 100644 --- a/dev/archery/archery/crossbow/reports.py +++ b/dev/archery/archery/crossbow/reports.py @@ -25,6 +25,7 @@ import fnmatch import functools import time +from urllib.parse import quote import click import requests @@ -82,6 +83,14 @@ def task_url(self, task): # show link to the actual build, some CI providers implement # the statuses API others implement the checks API, retrieve any. return build_links[0] + elif task.ci == 'github': + branch = quote(task.branch, safe='') + return f'{self.repo_url}/actions?query=branch:{branch}' + elif task.ci == 'azure': + repo = '/'.join(self.repo_url.rsplit('/', 2)[-2:]) + branch = quote(task.branch, safe='') + return (f'https://dev.azure.com/{repo}/_build/latest' + f'?definitionId=1&branchName={branch}') else: # show link to the branch if no status build link was found. return self.branch_url(task.branch) diff --git a/dev/archery/archery/crossbow/tests/test_reports.py b/dev/archery/archery/crossbow/tests/test_reports.py index 02012d2f1beb..922666ba5fb5 100644 --- a/dev/archery/archery/crossbow/tests/test_reports.py +++ b/dev/archery/archery/crossbow/tests/test_reports.py @@ -17,12 +17,87 @@ import re import textwrap +from unittest import mock + +import pytest from archery.crossbow.cli import (NightlyEmailReport, TokenExpirationEmailReport) from archery.crossbow.core import yaml from archery.crossbow.reports import (ChatReport, CommentReport, Report) +@pytest.mark.parametrize(('ci', 'expected_url'), [ + ('github', 'https://github.com/apache/crossbow/actions?query=branch:{branch}'), + ('azure', 'https://dev.azure.com/apache/crossbow/_build/latest' + '?definitionId=1&branchName={branch}'), + ('travis', 'https://github.com/apache/crossbow/tree/{branch}'), + ('circle', 'https://github.com/apache/crossbow/tree/{branch}'), + ('appveyor', 'https://github.com/apache/crossbow/tree/{branch}'), + ('drone', 'https://github.com/apache/crossbow/tree/{branch}'), +]) +def test_crossbow_task_url_without_build_link(load_fixture, ci, expected_url): + job = load_fixture('crossbow-job.yaml', decoder=yaml.load) + task = job.tasks['docker-cpp-cmake32'] + task.ci = ci + task.status().build_links = [] + + report = Report(job) + assert report.task_url(task) == expected_url.format(branch=task.branch) + + +@pytest.mark.parametrize(('ci', 'expected_url'), [ + ('github', 'https://github.com/apache/crossbow/actions?query=branch:' + 'feature%2Ffoo%2Bbar%26baz'), + ('azure', 'https://dev.azure.com/apache/crossbow/_build/latest' + '?definitionId=1&branchName=feature%2Ffoo%2Bbar%26baz'), +]) +def test_crossbow_task_url_escaped_branch(load_fixture, ci, expected_url): + job = load_fixture('crossbow-job.yaml', decoder=yaml.load) + job.queue._remote_url += '.git' + task = job.tasks['docker-cpp-cmake32'] + task.ci = ci + task.branch = 'feature/foo+bar&baz' + task.status().build_links = [] + + assert Report(job).task_url(task) == expected_url + + +def test_crossbow_task_url_build_link(load_fixture): + job = load_fixture('crossbow-job.yaml', decoder=yaml.load) + task = job.tasks['docker-cpp-cmake32'] + task.ci = 'github' + task.status().build_links = [ + 'https://github.com/apache/crossbow/actions/runs/1', + 'https://github.com/apache/crossbow/actions/runs/2', + ] + report = Report(job, wait_for_task=10) + + with mock.patch('archery.crossbow.reports.time.sleep') as sleep: + assert report.task_url(task) == task.status().build_links[0] + sleep.assert_not_called() + + +@pytest.mark.parametrize('build_links', [ + [], + ['https://github.com/apache/crossbow/actions/runs/1'], +]) +def test_crossbow_task_url_after_wait(load_fixture, build_links): + job = load_fixture('crossbow-job.yaml', decoder=yaml.load) + task = job.tasks['docker-cpp-cmake32'] + task.ci = 'github' + report = Report(job, wait_for_task=10) + expected_url = (build_links[0] if build_links else + f'https://github.com/apache/crossbow/actions' + f'?query=branch:{task.branch}') + + with mock.patch.object(task, 'status', side_effect=[ + mock.Mock(build_links=[]), mock.Mock(build_links=build_links) + ]) as status, mock.patch('archery.crossbow.reports.time.sleep') as sleep: + assert report.task_url(task) == expected_url + sleep.assert_called_once_with(10) + assert status.call_args_list == [mock.call(), mock.call(force_query=True)] + + def test_crossbow_comment_formatter(load_fixture): msg = load_fixture('crossbow-success-message.md') job = load_fixture('crossbow-job.yaml', decoder=yaml.load)