Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions bert_e/docs/USER_DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ __Bert-E__:
| command name | description | requires admin rights? |
|:--------------------- |:------------------------ |:----------------------:|
| help | Print __Bert-E__'s manual in the pull request | no
| status | Print everything still missing before this pull request can be merged (see below) | no
| reset | Let __Bert-E__ reset the integration branches associated to the current pull request with a warning if the developer manually modified one of the the integration branches | no
| force_reset | Let __Bert-E__ reset the integration branches associated to the current pull request **without warning**. | no

Expand All @@ -170,6 +171,29 @@ option -- that way, comments addressed to other bots (e.g.
that explicitly address __Bert-E__ via its ``@<robot>`` mention are always
dispatched normally and will still report unknown commands.

### The status command

``/status`` (or ``@bert-e status``) posts a report listing every check at
once, instead of only the next blocker. It is read-only: it never creates
integration branches, merges or queues anything, and any user can run it. A
new report is posted each time the command is issued.

Each row is marked :sunny: (satisfied, possibly with a note such as
"bypassed"), :exclamation: (missing) or :hourglass: (not yet evaluated). The
report covers:

* approvals (author, peer, leader, unanimity, requested changes);
* the ``wait`` option, when it is set;
* integration builds: every integration branch whose build is not
successful, with a link to its build when available. Before the integration
branches exist, this row shows as not yet evaluated;
* Jira fix versions, when Jira checks are configured;
* integration branch history (whether a reset may be needed);
* the merge queue, when queues are enabled.

This command is unrelated to the web status page served by __Bert-E__, which
shows the merge queue and recent merges for the whole repository.

Integration branches...
-----------------------
__*Bert-E* creates temporary branches during the merge process. These are
Expand Down
4 changes: 3 additions & 1 deletion bert_e/templates/status_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
check | status
---------|--------
{% for item in status -%}
:arrow_right: **{{status[item].display_name}}** | {% if status[item].pass %}:sunny:{% else %}:exclamation: {{ status[item].details | join(' — ') }}{% endif %}
:arrow_right: **{{status[item].display_name}}** | {% if status[item].pending %}:hourglass: {{ status[item].details | join(' — ') }}{% elif status[item].pass %}:sunny:{% if status[item].details %} {{ status[item].details | join(' — ') }}{% endif %}{% else %}:exclamation: {{ status[item].details | join(' — ') }}{% endif %}
{% endfor %}

:sunny: satisfied — :exclamation: missing — :hourglass: not yet evaluated

{% else %}

*Status report is not available.*
Expand Down
96 changes: 96 additions & 0 deletions bert_e/tests/test_bert_e.py
Original file line number Diff line number Diff line change
Expand Up @@ -3021,6 +3021,102 @@ def test_status_command_robot_commit_in_history(self):
self.assertIn('history', report)
self.assertTrue(getattr(report['history'], 'pass'))

def test_status_command_lists_every_failing_build(self):
"""Status report lists every failing integration build, not only the
worst one."""
pr = self.create_pr('bugfix/TEST-00001', 'development/4.3')
self.handle(pr.id, options=['bypass_jira_check'])
self.gitrepo.cmd('git fetch --prune')
wbranch_refs = self.gitrepo.cmd(
'git branch -r --list origin/w/*/bugfix/TEST-00001'
).strip().split()
self.assertGreaterEqual(len(wbranch_refs), 2)
ok_ref, failing_refs = wbranch_refs[0], wbranch_refs[1:]
sha = self.gitrepo.cmd('git rev-parse %s' % ok_ref).strip()
self.set_build_status(sha, 'SUCCESSFUL')
for ref in failing_refs:
sha = self.gitrepo.cmd('git rev-parse %s' % ref).strip()
self.set_build_status(sha, 'FAILED')
pr.add_comment('@%s status' % self.args.robot_username)
with self.assertRaises(exns.StatusReport) as ctx:
self.handle(pr.id, options=['bypass_jira_check'], backtrace=True)
builds = ctx.exception.kwargs['status']['builds']
self.assertFalse(getattr(builds, 'pass'))
self.assertEqual(len(builds.details), len(failing_refs))
for ref in failing_refs:
name = ref[len('origin/'):]
self.assertTrue(any(d.startswith(name + ': FAILED')
for d in builds.details))
self.assertIn('FAILED', ctx.exception.msg)

def test_status_command_builds_not_yet_evaluated(self):
"""Status report marks builds as pending when integration branches
do not exist yet, and does not create them."""
pr = self.create_pr('bugfix/my-feature', 'development/4.3')
# Stops at the Jira check, before integration branches are created.
self.handle(pr.id)
pr.add_comment('@%s status' % self.args.robot_username)
with self.assertRaises(exns.StatusReport) as ctx:
self.handle(pr.id, backtrace=True)
report = ctx.exception.kwargs['status']
self.assertIn('builds', report)
self.assertTrue(report['builds'].pending)
self.assertFalse(getattr(report['builds'], 'pass'))
self.assertTrue(
any('not yet evaluated' in d for d in report['builds'].details))
self.assertNotIn('history', report)
self.assertIn(':hourglass:', ctx.exception.msg)
self.gitrepo.cmd('git fetch --prune')
self.assertEqual(self.gitrepo.cmd(
'git branch -r --list origin/w/*/bugfix/my-feature').strip(), '')

def test_status_command_approvals_bypassed_details(self):
"""Status report says approvals are bypassed, not just satisfied."""
pr = self.create_pr('bugfix/TEST-00001', 'development/4.3')
self.handle(pr.id, options=['bypass_jira_check'])
pr.add_comment('@%s status' % self.args.robot_username)
with self.assertRaises(exns.StatusReport) as ctx:
self.handle(pr.id, options=['bypass_jira_check',
'bypass_author_approval',
'bypass_peer_approval',
'bypass_leader_approval'],
backtrace=True)
approvals = ctx.exception.kwargs['status']['approvals']
self.assertTrue(getattr(approvals, 'pass'))
self.assertTrue(any('bypassed' in d for d in approvals.details))
self.assertIn('bypassed', ctx.exception.msg)

def test_status_command_wait_option(self):
"""Status report lists the wait option as a blocker."""
pr = self.create_pr('bugfix/TEST-00001', 'development/4.3')
self.handle(pr.id, options=['bypass_jira_check'])
pr.add_comment('@%s status' % self.args.robot_username)
with self.assertRaises(exns.StatusReport) as ctx:
self.handle(pr.id, options=['bypass_jira_check', 'wait'],
backtrace=True)
report = ctx.exception.kwargs['status']
self.assertIn('wait', report)
self.assertFalse(getattr(report['wait'], 'pass'))

def test_status_command_no_wait_row_by_default(self):
"""Status report omits the wait row when the option is not set."""
pr = self.create_pr('bugfix/TEST-00001', 'development/4.3')
self.handle(pr.id, options=['bypass_jira_check'])
pr.add_comment('@%s status' % self.args.robot_username)
with self.assertRaises(exns.StatusReport) as ctx:
self.handle(pr.id, options=['bypass_jira_check'], backtrace=True)
self.assertNotIn('wait', ctx.exception.kwargs['status'])

def test_status_command_help_text(self):
"""The status command has a real description in the help page."""
pr = self.create_pr('bugfix/TEST-00001', 'development/4.3')
self.handle(pr.id, options=['bypass_jira_check'])
pr.add_comment('@%s help' % self.args.robot_username)
with self.assertRaises(exns.HelpMessage) as ctx:
self.handle(pr.id, options=['bypass_jira_check'], backtrace=True)
self.assertIn('Print everything still missing before this pull '
'request can be merged.', ctx.exception.msg)

def test_bypass_options(self):
# test bypass all approvals through an incorrect bitbucket comment
pr = self.create_pr('bugfix/TEST-00001', 'development/4.3')
Expand Down
88 changes: 88 additions & 0 deletions bert_e/tests/unit/test_status_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Unit tests for the rows of the /status report."""
from types import SimpleNamespace
from unittest.mock import Mock, patch

from bert_e import exceptions as exns
from bert_e.workflow.gitwaterflow import commands


def _job(**settings):
defaults = dict(build_key='pre-merge', wait=False, use_queue=False,
bypass_build_status=False)
defaults.update(settings)
return SimpleNamespace(settings=SimpleNamespace(**defaults),
project_repo=Mock(), active_options=[],
author_bypass={})


def _branch(name, sha):
return SimpleNamespace(name=name, get_latest_commit=lambda: sha)


def test_builds_lists_every_failing_branch_with_link():
job = _job()
states = {'a': 'SUCCESSFUL', 'b': 'FAILED', 'c': 'INPROGRESS'}
job.project_repo.get_build_status.side_effect = lambda sha, key: states[
sha]
job.project_repo.get_build_url.side_effect = (
lambda sha, key: 'https://ci/{}'.format(sha) if sha == 'b' else None)
branches = [_branch('w/1', 'a'), _branch('w/2', 'b'),
_branch('w/3', 'c')]
with patch.object(commands, 'get_integration_branches',
return_value=branches):
item = commands._check_builds_status(job)
assert getattr(item, 'pass') is False
assert not item.pending
assert item.details == ['w/2: FAILED ([build](https://ci/b))',
'w/3: INPROGRESS']


def test_builds_pending_without_integration_branches():
job = _job()
with patch.object(commands, 'get_integration_branches',
return_value=[]):
item = commands._check_builds_status(job)
assert item.pending
assert 'not yet evaluated' in item.details[0]


def test_wait_row():
assert commands._check_wait_status(_job()) is None
item = commands._check_wait_status(_job(wait=True))
assert getattr(item, 'pass') is False


def test_queue_row_disabled():
assert commands._check_queue_status(_job()) is None


def test_queue_row_states():
job = _job(use_queue=True)
branches = [_branch('w/1', 'a')]
with patch.object(commands, 'get_integration_branches',
return_value=branches), \
patch('bert_e.workflow.gitwaterflow.queueing.already_in_queue',
return_value=True):
item = commands._check_queue_status(job)
assert getattr(item, 'pass') and item.details == ['queued']
with patch.object(commands, 'get_integration_branches',
return_value=branches), \
patch('bert_e.workflow.gitwaterflow.queueing.already_in_queue',
return_value=False):
item = commands._check_queue_status(job)
assert item.pending


def test_template_renders_every_state():
report = {
'ok': commands._StatusItem('Satisfied', True),
'bypassed': commands._StatusItem('Bypassed', True, ['bypassed']),
'ko': commands._StatusItem('Missing', False, ['author approval']),
'wait': commands._StatusItem('Pending', False, ['not yet'],
pending=True),
}
msg = exns.StatusReport(status=report, active_options=[]).msg
assert '**Satisfied** | :sunny:\n' in msg
assert '**Bypassed** | :sunny: bypassed' in msg
assert '**Missing** | :exclamation: author approval' in msg
assert '**Pending** | :hourglass: not yet' in msg
Loading
Loading