mirror of
https://github.com/odoo/runbot.git
synced 2025-03-16 16:05:42 +07:00

The staging cron turns out to be pretty reasonable to trigger, as we already have a handler on the transition of a batch to `not blocked`, which is exactly when we want to create a staging (that and the completion of the previous staging). The batch transition is in a compute which is not awesome, but on the flip side we also cancel active stagings in that exact scenario (if it applies), so that matches. The only real finesse is that one of the tests wants to observe the instant between the end of a staging (and creation of splits) and the start of the next one, which because the staging cron is triggered by the failure of the previous staging is now "atomic", requiring disabling the staging cron, which means the trigger is skipped entirely. So this requires triggering the staging cron by hand.
1539 lines
59 KiB
Python
1539 lines
59 KiB
Python
""" The mergebot does not work on a dependency basis, rather all
|
|
repositories of a project are co-equal and get (on target and
|
|
source branches).
|
|
|
|
When preparing a staging, we simply want to ensure branch-matched PRs
|
|
are staged concurrently in all repos
|
|
"""
|
|
import functools
|
|
import operator
|
|
import time
|
|
import xmlrpc.client
|
|
from itertools import repeat
|
|
|
|
import pytest
|
|
import requests
|
|
from lxml.etree import XPath
|
|
|
|
from utils import seen, get_partner, pr_page, to_pr, Commit
|
|
|
|
|
|
@pytest.fixture
|
|
def repo_a(env, project, make_repo, setreviewers):
|
|
repo = make_repo('a')
|
|
r = env['runbot_merge.repository'].create({
|
|
'project_id': project.id,
|
|
'name': repo.name,
|
|
'required_statuses': 'default',
|
|
'group_id': False,
|
|
})
|
|
setreviewers(r)
|
|
env['runbot_merge.events_sources'].create({'repository': r.name})
|
|
return repo
|
|
|
|
@pytest.fixture
|
|
def repo_b(env, project, make_repo, setreviewers):
|
|
repo = make_repo('b')
|
|
r = env['runbot_merge.repository'].create({
|
|
'project_id': project.id,
|
|
'name': repo.name,
|
|
'required_statuses': 'default',
|
|
'group_id': False,
|
|
})
|
|
setreviewers(r)
|
|
env['runbot_merge.events_sources'].create({'repository': r.name})
|
|
return repo
|
|
|
|
@pytest.fixture
|
|
def repo_c(env, project, make_repo, setreviewers):
|
|
repo = make_repo('c')
|
|
r = env['runbot_merge.repository'].create({
|
|
'project_id': project.id,
|
|
'name': repo.name,
|
|
'required_statuses': 'default',
|
|
'group_id': False,
|
|
})
|
|
setreviewers(r)
|
|
env['runbot_merge.events_sources'].create({'repository': r.name})
|
|
return repo
|
|
|
|
def make_pr(repo, prefix, trees, *, target='master', user,
|
|
statuses=(('default', 'success'),),
|
|
reviewer):
|
|
"""
|
|
:type repo: fake_github.Repo
|
|
:type prefix: str
|
|
:type trees: list[dict]
|
|
:type target: str
|
|
:type user: str
|
|
:type statuses: list[(str, str)]
|
|
:type reviewer: str | None
|
|
:rtype: fake_github.PR
|
|
"""
|
|
*_, c = repo.make_commits(
|
|
'heads/{}'.format(target),
|
|
*(
|
|
repo.Commit('commit_{}_{:02}'.format(prefix, i), tree=tree)
|
|
for i, tree in enumerate(trees)
|
|
),
|
|
ref='heads/{}'.format(prefix)
|
|
)
|
|
pr = repo.make_pr(title='title {}'.format(prefix), body='body {}'.format(prefix),
|
|
target=target, head=prefix, token=user)
|
|
for context, result in statuses:
|
|
repo.post_status(c, result, context)
|
|
if reviewer:
|
|
pr.post_comment('hansen r+', reviewer)
|
|
return pr
|
|
|
|
|
|
@pytest.mark.parametrize('uniquifier', [False, True])
|
|
def test_stage_one(env, project, repo_a, repo_b, config, uniquifier):
|
|
""" First PR is non-matched from A => should not select PR from B
|
|
"""
|
|
project.uniquifier = uniquifier
|
|
project.batch_limit = 1
|
|
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
pr_a = make_pr(
|
|
repo_a, 'A', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'])
|
|
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
pr_b = make_pr(
|
|
repo_b, 'B', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
pra_id = to_pr(env, pr_a)
|
|
assert pra_id.state == 'ready'
|
|
assert pra_id.staging_id
|
|
assert repo_a.commit('staging.master').message.startswith('commit_A_00')
|
|
if uniquifier:
|
|
assert repo_b.commit('staging.master').message.startswith('force rebuild')
|
|
else:
|
|
assert repo_b.commit('staging.master').message == 'initial'
|
|
|
|
prb_id = to_pr(env, pr_b)
|
|
assert prb_id.state == 'ready'
|
|
assert not prb_id.staging_id
|
|
|
|
get_related_pr_labels = XPath('.//*[normalize-space(text()) = "Linked pull requests"]//a/text()')
|
|
def test_stage_match(env, project, repo_a, repo_b, config, page):
|
|
""" First PR is matched from A, => should select matched PR from B
|
|
"""
|
|
project.batch_limit = 1
|
|
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
prx_a = make_pr(
|
|
repo_a, 'do-a-thing', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
prx_b = make_pr(repo_b, 'do-a-thing', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
pr_a = to_pr(env, prx_a)
|
|
pr_b = to_pr(env, prx_b)
|
|
|
|
# check that related PRs link to one another
|
|
assert get_related_pr_labels(pr_page(page, prx_a)) == pr_b.mapped('display_name')
|
|
assert get_related_pr_labels(pr_page(page, prx_b)) == pr_a.mapped('display_name')
|
|
|
|
env.run_crons()
|
|
|
|
assert pr_a.state == 'ready'
|
|
assert pr_a.staging_id
|
|
assert pr_b.state == 'ready'
|
|
assert pr_b.staging_id
|
|
# should be part of the same staging
|
|
assert pr_a.staging_id == pr_b.staging_id, \
|
|
"branch-matched PRs should be part of the same staging"
|
|
|
|
# check that related PRs *still* link to one another during staging
|
|
assert get_related_pr_labels(pr_page(page, prx_a)) == [pr_b.display_name]
|
|
assert get_related_pr_labels(pr_page(page, prx_b)) == [pr_a.display_name]
|
|
with repo_a:
|
|
repo_a.post_status('staging.master', 'failure')
|
|
env.run_crons()
|
|
|
|
assert pr_a.state == 'error'
|
|
assert pr_b.state == 'ready'
|
|
|
|
with repo_a:
|
|
prx_a.post_comment('hansen retry', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
|
|
assert pr_a.state == pr_b.state == 'ready'
|
|
assert pr_a.staging_id and pr_b.staging_id
|
|
for repo in [repo_a, repo_b]:
|
|
with repo:
|
|
repo.post_status('staging.master', 'success')
|
|
env.run_crons()
|
|
assert pr_a.state == 'merged'
|
|
assert pr_b.state == 'merged'
|
|
|
|
assert 'Related: {}'.format(pr_b.display_name) in repo_a.commit('master').message
|
|
assert 'Related: {}'.format(pr_a.display_name) in repo_b.commit('master').message
|
|
|
|
# check that related PRs *still* link to one another after merge
|
|
assert get_related_pr_labels(pr_page(page, prx_a)) == [pr_b.display_name]
|
|
assert get_related_pr_labels(pr_page(page, prx_b)) == [pr_a.display_name]
|
|
|
|
def test_different_targets(env, project, repo_a, repo_b, config):
|
|
""" PRs with different targets should not be matched together
|
|
"""
|
|
project.write({
|
|
'batch_limit': 1,
|
|
'branch_ids': [(0, 0, {'name': 'other'})]
|
|
})
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'master': 'a_0'}), ref='heads/master')
|
|
repo_a.make_commits(None, Commit('initial', tree={'other': 'a_0'}), ref='heads/other')
|
|
pr_a = make_pr(
|
|
repo_a, 'do-a-thing', [{'mater': 'a_1'}],
|
|
target='master',
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'master': 'b_0'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'other': 'b_0'}), ref='heads/other')
|
|
pr_b = make_pr(
|
|
repo_b, 'do-a-thing', [{'other': 'b_1'}],
|
|
target='other',
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
statuses=[],
|
|
)
|
|
time.sleep(5)
|
|
env.run_crons()
|
|
|
|
pr_a = to_pr(env, pr_a)
|
|
pr_b = to_pr(env, pr_b)
|
|
assert pr_a.state == 'ready'
|
|
assert not pr_a.blocked
|
|
assert pr_a.staging_id
|
|
|
|
assert pr_b.blocked
|
|
assert pr_b.state == 'approved'
|
|
assert not pr_b.staging_id
|
|
|
|
for r in [repo_a, repo_b]:
|
|
with r:
|
|
r.post_status('staging.master', 'success')
|
|
env.run_crons()
|
|
assert pr_a.state == 'merged'
|
|
|
|
def test_stage_different_statuses(env, project, repo_a, repo_b, config):
|
|
project.batch_limit = 1
|
|
|
|
env['runbot_merge.repository'].search([
|
|
('name', '=', repo_b.name)
|
|
]).write({
|
|
'required_statuses': 'foo/bar',
|
|
})
|
|
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
pr_a = make_pr(
|
|
repo_a, 'do-a-thing', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
repo_a.post_status(pr_a.head, 'success', 'foo/bar')
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
[c] = repo_b.make_commits(
|
|
'heads/master',
|
|
repo_b.Commit(f'some_commit\n\nSee also {repo_a.name}#{pr_a.number:d}', tree={'a': 'b_1'}),
|
|
ref='heads/do-a-thing'
|
|
)
|
|
pr_b = repo_b.make_pr(
|
|
title="title", body="body", target='master', head='do-a-thing',
|
|
token=config['role_user']['token'])
|
|
repo_b.post_status(c, 'success')
|
|
pr_b.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
# since the labels are the same but the statuses on pr_b are not the
|
|
# expected ones, pr_a should be blocked on pr_b, which should be approved
|
|
# but not validated / ready
|
|
pr_a_id = to_pr(env, pr_a)
|
|
pr_b_id = to_pr(env, pr_b)
|
|
assert pr_a_id.state == 'ready'
|
|
assert not pr_a_id.staging_id
|
|
assert pr_a_id.blocked
|
|
assert pr_b_id.state == 'approved'
|
|
assert not pr_b_id.staging_id
|
|
|
|
with repo_b:
|
|
repo_b.post_status(pr_b.head, 'success', 'foo/bar')
|
|
env.run_crons()
|
|
|
|
assert pr_a_id.state == pr_b_id.state == 'ready'
|
|
assert pr_a_id.staging_id == pr_b_id.staging_id
|
|
|
|
# do the actual merge to check for the Related header
|
|
for repo in [repo_a, repo_b]:
|
|
with repo:
|
|
repo.post_status('staging.master', 'success')
|
|
repo.post_status('staging.master', 'success', 'foo/bar')
|
|
env.run_crons()
|
|
|
|
pr_a_ref = to_pr(env, pr_a).display_name
|
|
pr_b_ref = to_pr(env, pr_b).display_name
|
|
master_a = repo_a.commit('master')
|
|
master_b = repo_b.commit('master')
|
|
|
|
assert 'Related: {}'.format(pr_b_ref) in master_a.message,\
|
|
"related should be in PR A's message"
|
|
assert 'Related: {}'.format(pr_a_ref) not in master_b.message,\
|
|
"related should not be in PR B's message since the ref' was added explicitly"
|
|
assert pr_a_ref in master_b.message, "the ref' should still be there though"
|
|
|
|
def test_unmatch_patch(env, project, repo_a, repo_b, config):
|
|
""" When editing files via the UI for a project you don't have write
|
|
access to, a branch called patch-XXX is automatically created in your
|
|
profile to hold the change.
|
|
|
|
This means it's possible to create a:patch-1 and b:patch-1 without
|
|
intending them to be related in any way, and more likely than the opposite
|
|
since there is no user control over the branch names (save by actually
|
|
creating/renaming branches afterwards before creating the PR).
|
|
|
|
-> PRs with a branch name of patch-* should not be label-matched
|
|
"""
|
|
project.batch_limit = 1
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
pr_a = make_pr(
|
|
repo_a, 'patch-1', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref=f'heads/master')
|
|
pr_b = make_pr(
|
|
repo_b, 'patch-1', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
pr_a = to_pr(env, pr_a)
|
|
pr_b = to_pr(env, pr_b)
|
|
assert pr_a.state == 'ready'
|
|
assert pr_a.staging_id
|
|
assert pr_b.state == 'ready'
|
|
assert not pr_b.staging_id, 'patch-* PRs should not be branch-matched'
|
|
|
|
def test_sub_match(env, project, repo_a, repo_b, repo_c, config):
|
|
""" Branch-matching should work on a subset of repositories
|
|
"""
|
|
project.batch_limit = 1
|
|
with repo_a: # no pr here
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
pr_b = make_pr(
|
|
repo_b, 'do-a-thing', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
with repo_c:
|
|
repo_c.make_commits(None, Commit('initial', tree={'a': 'c_0'}), ref='heads/master')
|
|
pr_c = make_pr(
|
|
repo_c, 'do-a-thing', [{'a': 'c_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
pr_b = to_pr(env, pr_b)
|
|
pr_c = to_pr(env, pr_c)
|
|
assert pr_b.state == 'ready'
|
|
assert pr_b.staging_id
|
|
assert pr_c.state == 'ready'
|
|
assert pr_c.staging_id
|
|
# should be part of the same staging
|
|
assert pr_c.staging_id == pr_b.staging_id, \
|
|
"branch-matched PRs should be part of the same staging"
|
|
|
|
st = pr_b.staging_id
|
|
a_staging = repo_a.commit('staging.master')
|
|
b_staging = repo_b.commit('staging.master')
|
|
c_staging = repo_c.commit('staging.master')
|
|
assert sorted(st.head_ids.mapped('sha')) == sorted([
|
|
a_staging.id,
|
|
b_staging.id,
|
|
c_staging.id,
|
|
])
|
|
s = env['runbot_merge.stagings'].for_heads(
|
|
a_staging.id,
|
|
b_staging.id,
|
|
c_staging.id,
|
|
)
|
|
assert s == list(st.ids)
|
|
|
|
assert sorted(st.commit_ids.mapped('sha')) == sorted([
|
|
a_staging.parents[0],
|
|
b_staging.id,
|
|
c_staging.id,
|
|
])
|
|
s = env['runbot_merge.stagings'].for_commits(
|
|
a_staging.parents[0],
|
|
b_staging.id,
|
|
c_staging.id,
|
|
)
|
|
assert s == list(st.ids)
|
|
|
|
|
|
def test_merge_fail(env, project, repo_a, repo_b, users, config):
|
|
""" In a matched-branch scenario, if merging in one of the linked repos
|
|
fails it should revert the corresponding merges
|
|
"""
|
|
project.batch_limit = 1
|
|
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
|
|
# first set of matched PRs
|
|
pr1a = make_pr(
|
|
repo_a, 'do-a-thing', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
pr1b = make_pr(
|
|
repo_b, 'do-a-thing', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
|
|
# add a conflicting commit to B so the staging fails
|
|
repo_b.make_commit('heads/master', 'cn', None, tree={'a': 'cn'})
|
|
|
|
# and a second set of PRs which should get staged while the first set
|
|
# fails
|
|
pr2a = make_pr(
|
|
repo_a, 'do-b-thing', [{'b': 'ok'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
pr2b = make_pr(
|
|
repo_b, 'do-b-thing', [{'b': 'ok'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
s2 = to_pr(env, pr2a) | to_pr(env, pr2b)
|
|
st = env['runbot_merge.stagings'].search([])
|
|
assert set(st.batch_ids.prs.ids) == set(s2.ids)
|
|
|
|
failed = to_pr(env, pr1b)
|
|
assert failed.state == 'error'
|
|
assert pr1b.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, pr1b, users),
|
|
(users['user'], '@%(user)s @%(reviewer)s unable to stage: merge conflict' % users),
|
|
]
|
|
other = to_pr(env, pr1a)
|
|
reviewer = get_partner(env, users["reviewer"]).formatted_email
|
|
assert not other.staging_id
|
|
assert [
|
|
c['commit']['message']
|
|
for c in repo_a.log('heads/staging.master')
|
|
] == [
|
|
"""commit_do-b-thing_00
|
|
|
|
closes %s
|
|
|
|
Related: %s
|
|
Signed-off-by: %s""" % (s2[0].display_name, s2[1].display_name, reviewer),
|
|
'initial'
|
|
], "dummy commit + squash-merged PR commit + root commit"
|
|
|
|
def test_ff_fail(env, project, repo_a, repo_b, config):
|
|
""" In a matched-branch scenario, fast-forwarding one of the repos fails
|
|
the entire thing should be rolled back
|
|
"""
|
|
project.batch_limit = 1
|
|
|
|
with repo_a, repo_b:
|
|
[root_a] = repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
make_pr(
|
|
repo_a, 'do-a-thing', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref=f'heads/master')
|
|
make_pr(
|
|
repo_b, 'do-a-thing', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
# add second commit blocking FF
|
|
with repo_b:
|
|
cn = repo_b.make_commit('heads/master', 'second', None, tree={'a': 'b_0', 'b': 'other'})
|
|
assert repo_b.commit('heads/master').id == cn
|
|
|
|
with repo_a, repo_b:
|
|
repo_a.post_status('heads/staging.master', 'success')
|
|
repo_b.post_status('heads/staging.master', 'success')
|
|
env.run_crons(None)
|
|
assert repo_b.commit('heads/master').id == cn,\
|
|
"B should still be at the conflicting commit"
|
|
assert repo_a.commit('heads/master').id == root_a,\
|
|
"FF A should have been rolled back when B failed"
|
|
|
|
# should be re-staged
|
|
st = env['runbot_merge.stagings'].search([])
|
|
assert len(st) == 1
|
|
assert len(st.batch_ids.prs) == 2
|
|
|
|
class TestCompanionsNotReady:
|
|
def test_one_pair(self, env, project, repo_a, repo_b, config, users):
|
|
""" If the companion of a ready branch-matched PR is not ready,
|
|
they should not get staged
|
|
"""
|
|
project.batch_limit = 1
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
# pr_a is born ready
|
|
p_a = make_pr(
|
|
repo_a, 'do-a-thing', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
p_b = make_pr(
|
|
repo_b, 'do-a-thing', [{'a': 'b_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=None,
|
|
)
|
|
|
|
pr_a = to_pr(env, p_a)
|
|
pr_b = to_pr(env, p_b)
|
|
assert pr_a.label == pr_b.label == '{}:do-a-thing'.format(config['github']['owner'])
|
|
|
|
env.run_crons()
|
|
|
|
assert pr_a.state == 'ready'
|
|
assert pr_b.state == 'validated'
|
|
assert not pr_b.staging_id
|
|
assert not pr_a.staging_id, \
|
|
"pr_a should not have been staged as companion is not ready"
|
|
|
|
assert p_a.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, p_a, users),
|
|
(users['user'], "@%s @%s linked pull request(s) %s not ready. Linked PRs are not staged until all of them are ready." % (
|
|
users['user'],
|
|
users['reviewer'],
|
|
pr_b.display_name,
|
|
)),
|
|
]
|
|
# ensure the message is only sent once per PR
|
|
env.run_crons('runbot_merge.check_linked_prs_status')
|
|
assert p_a.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, p_a, users),
|
|
(users['user'], "@%s @%s linked pull request(s) %s not ready. Linked PRs are not staged until all of them are ready." % (
|
|
users['user'],
|
|
users['reviewer'],
|
|
pr_b.display_name,
|
|
)),
|
|
]
|
|
assert p_b.comments == [seen(env, p_b, users)]
|
|
|
|
def test_two_of_three_unready(self, env, project, repo_a, repo_b, repo_c, users, config):
|
|
""" In a 3-batch, if two of the PRs are not ready both should be
|
|
linked by the first one
|
|
"""
|
|
project.batch_limit = 1
|
|
with repo_a, repo_b, repo_c:
|
|
repo_a.make_commits(None, Commit('initial', tree={'f': 'a0'}), ref='heads/master')
|
|
pr_a = make_pr(
|
|
repo_a, 'a-thing', [{'f': 'a1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=None,
|
|
)
|
|
|
|
repo_b.make_commits(None, Commit('initial', tree={'f': 'b0'}), ref='heads/master')
|
|
pr_b = make_pr(
|
|
repo_b, 'a-thing', [{'f': 'b1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
|
|
repo_c.make_commits(None, Commit('initial', tree={'f': 'c0'}), ref='heads/master')
|
|
pr_c = make_pr(
|
|
repo_c, 'a-thing', [{'f': 'c1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=None,
|
|
)
|
|
env.run_crons()
|
|
|
|
assert pr_a.comments == [seen(env, pr_a, users)]
|
|
assert pr_b.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, pr_b, users),
|
|
(users['user'], "@%s @%s linked pull request(s) %s#%d, %s#%d not ready. Linked PRs are not staged until all of them are ready." % (
|
|
users['user'], users['reviewer'],
|
|
repo_a.name, pr_a.number,
|
|
repo_c.name, pr_c.number
|
|
))
|
|
]
|
|
assert pr_c.comments == [seen(env, pr_c, users)]
|
|
|
|
def test_one_of_three_unready(self, env, project, repo_a, repo_b, repo_c, users, config):
|
|
""" In a 3-batch, if one PR is not ready it should be linked on the
|
|
other two
|
|
"""
|
|
project.batch_limit = 1
|
|
with repo_a, repo_b, repo_c:
|
|
repo_a.make_commits(None, Commit('initial', tree={'f': 'a0'}), ref='heads/master')
|
|
pr_a = make_pr(
|
|
repo_a, 'a-thing', [{'f': 'a1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=None,
|
|
)
|
|
|
|
repo_b.make_commits(None, Commit('initial', tree={'f': 'b0'}), ref='heads/master')
|
|
pr_b = make_pr(
|
|
repo_b, 'a-thing', [{'f': 'b1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
|
|
repo_c.make_commits(None, Commit('initial', tree={'f': 'c0'}), ref='heads/master')
|
|
pr_c = make_pr(
|
|
repo_c, 'a-thing', [{'f': 'c1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
assert pr_a.comments == [seen(env, pr_a, users)]
|
|
assert pr_b.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, pr_b, users),
|
|
(users['user'], f"@{users['user']} @{users['reviewer']} linked pull request(s) {repo_a.name}#{pr_a.number} not ready. Linked PRs are not staged until all of them are ready.")
|
|
]
|
|
assert pr_c.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, pr_c, users),
|
|
(users['user'],
|
|
f"@{users['user']} @{users['reviewer']} linked pull request(s) {repo_a.name}#{pr_a.number} not ready. Linked PRs are not staged until all of them are ready.")
|
|
]
|
|
|
|
def test_other_failed(env, project, repo_a, repo_b, users, config):
|
|
""" In a non-matched-branch scenario, if the companion staging (copy of
|
|
targets) fails when built with the PR, it should provide a non-useless
|
|
message
|
|
"""
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a_0'}), ref='heads/master')
|
|
# pr_a is born ready
|
|
pr_a = make_pr(
|
|
repo_a, 'do-a-thing', [{'a': 'a_1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
|
|
repo_b.make_commits(None, Commit('initial', tree={'a': 'b_0'}), ref='heads/master')
|
|
env.run_crons()
|
|
|
|
pr = to_pr(env, pr_a)
|
|
assert pr.staging_id
|
|
|
|
with repo_a, repo_b:
|
|
repo_a.post_status('heads/staging.master', 'success', target_url="http://example.org/a")
|
|
repo_b.post_status('heads/staging.master', 'failure', target_url="http://example.org/b")
|
|
env.run_crons()
|
|
|
|
sth = repo_b.commit('heads/staging.master').id
|
|
assert not pr.staging_id
|
|
assert pr.state == 'error'
|
|
assert pr_a.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, pr_a, users),
|
|
(users['user'], '@%s @%s staging failed: default on %s (view more at http://example.org/b)' % (
|
|
users['user'], users['reviewer'],
|
|
sth
|
|
))
|
|
]
|
|
|
|
class TestMultiBatches:
|
|
def test_batching(self, env, project, repo_a, repo_b, config):
|
|
""" If multiple batches (label groups) are ready they should get batched
|
|
together (within the limits of teh project's batch limit)
|
|
"""
|
|
project.batch_limit = 3
|
|
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a0'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b': 'b0'}), ref='heads/master')
|
|
|
|
prs = [(
|
|
a and make_pr(repo_a, 'batch{}'.format(i), [{'a{}'.format(i): 'a{}'.format(i)}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token']),
|
|
b and make_pr(repo_b, 'batch{}'.format(i), [{'b{}'.format(i): 'b{}'.format(i)}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token']),
|
|
)
|
|
for i, (a, b) in enumerate([(1, 1), (0, 1), (1, 1), (1, 1), (1, 0)])
|
|
]
|
|
env.run_crons()
|
|
prs = [
|
|
(a and to_pr(env, a), b and to_pr(env, b))
|
|
for (a, b) in prs
|
|
]
|
|
|
|
st = env['runbot_merge.stagings'].search([])
|
|
assert st
|
|
assert len(st.batch_ids) == 3,\
|
|
"Should have batched the first <batch_limit> batches"
|
|
assert st.mapped('batch_ids.prs') == (
|
|
prs[0][0] | prs[0][1]
|
|
| prs[1][1]
|
|
| prs[2][0] | prs[2][1]
|
|
)
|
|
|
|
assert not prs[3][0].staging_id
|
|
assert not prs[3][1].staging_id
|
|
assert not prs[4][0].staging_id
|
|
|
|
def test_batching_split(self, env, repo_a, repo_b, config):
|
|
""" If a staging fails, it should get split properly across repos
|
|
"""
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': 'a0'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b': 'b0'}), ref='heads/master')
|
|
|
|
prs = [(
|
|
a and make_pr(repo_a, 'batch{}'.format(i), [{'a{}'.format(i): 'a{}'.format(i)}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token']),
|
|
b and make_pr(repo_b, 'batch{}'.format(i), [{'b{}'.format(i): 'b{}'.format(i)}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token']),
|
|
)
|
|
for i, (a, b) in enumerate([(1, 1), (0, 1), (1, 1), (1, 1), (1, 0)])
|
|
]
|
|
env.run_crons()
|
|
prs = [
|
|
(a and to_pr(env, a), b and to_pr(env, b))
|
|
for (a, b) in prs
|
|
]
|
|
|
|
st0 = env['runbot_merge.stagings'].search([])
|
|
assert len(st0.batch_ids) == 5
|
|
assert len(st0.mapped('batch_ids.prs')) == 8
|
|
|
|
# mark b.staging as failed -> should create two splits with (0, 1)
|
|
# and (2, 3, 4) and stage the first one
|
|
with repo_b:
|
|
repo_b.post_status('heads/staging.master', 'failure')
|
|
env.run_crons()
|
|
|
|
assert not st0.active
|
|
|
|
# at this point we have a re-staged split and an unstaged split
|
|
st = env['runbot_merge.stagings'].search([])
|
|
sp = env['runbot_merge.split'].search([])
|
|
assert st
|
|
assert sp
|
|
|
|
assert len(st.batch_ids) == 2
|
|
assert st.mapped('batch_ids.prs') == \
|
|
prs[0][0] | prs[0][1] | prs[1][1]
|
|
|
|
assert len(sp.batch_ids) == 3
|
|
assert sp.mapped('batch_ids.prs') == \
|
|
prs[2][0] | prs[2][1] | prs[3][0] | prs[3][1] | prs[4][0]
|
|
|
|
@pytest.mark.usefixtures("reviewer_admin")
|
|
def test_urgent(env, repo_a, repo_b, config):
|
|
""" Either PR of a co-dependent pair being prioritised leads to the entire
|
|
pair being prioritized
|
|
"""
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a0': 'a'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b0': 'b'}), ref='heads/master')
|
|
|
|
pr_a = make_pr(repo_a, 'batch', [{'a1': 'a'}, {'a2': 'a'}], user=config['role_user']['token'], reviewer=None, statuses=[])
|
|
pr_b = make_pr(repo_b, 'batch', [{'b1': 'b'}, {'b2': 'b'}], user=config['role_user']['token'], reviewer=None, statuses=[])
|
|
pr_c = make_pr(repo_a, 'C', [{'c1': 'c', 'c2': 'c'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'])
|
|
|
|
pr_a.post_comment('hansen rebase-merge', config['role_reviewer']['token'])
|
|
pr_b.post_comment('hansen rebase-merge alone skipchecks', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
|
|
p_a, p_b, p_c = to_pr(env, pr_a), to_pr(env, pr_b), to_pr(env, pr_c)
|
|
assert not p_a.blocked
|
|
assert not p_b.blocked
|
|
|
|
assert p_a.staging_id and p_b.staging_id and p_a.staging_id == p_b.staging_id,\
|
|
"a and b should be staged despite neither beinbg reviewed or approved"
|
|
assert p_a.batch_id and p_b.batch_id and p_a.batch_id == p_b.batch_id,\
|
|
"a and b should have been recognised as co-dependent"
|
|
assert not p_c.staging_id
|
|
|
|
with repo_a:
|
|
pr_a.post_comment('hansen r-', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
assert not p_b.staging_id.active, "should be unstaged"
|
|
assert p_b.priority == 'alone', "priority should not be affected anymore"
|
|
assert not p_b.skipchecks, "r- of linked pr should have un-skipcheck-ed this one"
|
|
assert p_a.blocked
|
|
assert p_b.blocked
|
|
|
|
class TestBlocked:
|
|
def test_merge_method(self, env, repo_a, config):
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a0': 'a'}), ref='heads/master')
|
|
|
|
pr = make_pr(repo_a, 'A', [{'a1': 'a'}, {'a2': 'a'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'],)
|
|
env.run_crons()
|
|
|
|
p = to_pr(env, pr)
|
|
assert p.state == 'ready'
|
|
assert p.blocked
|
|
|
|
with repo_a: pr.post_comment('hansen rebase-merge', config['role_reviewer']['token'])
|
|
assert not p.blocked
|
|
|
|
def test_linked_closed(self, env, repo_a, repo_b, config):
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a0': 'a'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b0': 'b'}), ref='heads/master')
|
|
|
|
pr1_a = make_pr(repo_a, 'xxx', [{'a1': 'a'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'],)
|
|
pr1_b = make_pr(repo_b, 'xxx', [{'b1': 'b'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'], statuses=[])
|
|
env.run_crons()
|
|
|
|
head_a = repo_a.commit('master').id
|
|
head_b = repo_b.commit('master').id
|
|
pr1_a_id = to_pr(env, pr1_a)
|
|
pr1_b_id = to_pr(env, pr1_b)
|
|
assert pr1_a_id.blocked
|
|
with repo_b: pr1_b.close()
|
|
assert not pr1_a_id.blocked
|
|
assert len(pr1_a_id.batch_id.all_prs) == 2
|
|
assert pr1_a_id.state == 'ready'
|
|
assert pr1_b_id.state == 'closed'
|
|
env.run_crons()
|
|
assert pr1_a_id.staging_id
|
|
with repo_a, repo_b:
|
|
repo_a.post_status('staging.master', 'success')
|
|
repo_b.post_status('staging.master', 'success')
|
|
env.run_crons()
|
|
assert pr1_a_id.state == 'merged'
|
|
assert pr1_a_id.batch_id.merge_date
|
|
assert repo_a.commit('master').id != head_a, \
|
|
"the master of repo A should be updated"
|
|
assert repo_b.commit('master').id == head_b, \
|
|
"the master of repo B should not be updated"
|
|
|
|
with repo_a:
|
|
pr2_a = make_pr(repo_a, "xxx", [{'x': 'x'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
pr2_a_id = to_pr(env, pr2_a)
|
|
assert pr2_a_id.batch_id != pr1_a_id.batch_id
|
|
assert pr2_a_id.label == pr1_a_id.label
|
|
assert len(pr2_a_id.batch_id.all_prs) == 1
|
|
|
|
def test_linked_merged(self, env, repo_a, repo_b, config):
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a0': 'a'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b0': 'b'}), ref='heads/master')
|
|
|
|
b = make_pr(repo_b, 'xxx', [{'b1': 'b'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'],)
|
|
env.run_crons() # stage b and c
|
|
|
|
with repo_a, repo_b:
|
|
repo_a.post_status('heads/staging.master', 'success')
|
|
repo_b.post_status('heads/staging.master', 'success')
|
|
env.run_crons() # merge b and c
|
|
assert to_pr(env, b).state == 'merged'
|
|
|
|
with repo_a:
|
|
pr = make_pr(repo_a, 'xxx', [{'a1': 'a'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'],)
|
|
env.run_crons() # merge b and c
|
|
|
|
p = to_pr(env, pr)
|
|
assert not p.blocked
|
|
|
|
@pytest.mark.usefixtures("reviewer_admin")
|
|
def test_linked_unready(self, env, repo_a, repo_b, config):
|
|
""" Create a PR A linked to a non-ready PR B,
|
|
* A is blocked by default
|
|
* A is not blocked if A.skipci
|
|
* A is not blocked if B.skipci
|
|
"""
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a0': 'a'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b0': 'b'}), ref='heads/master')
|
|
|
|
a = make_pr(repo_a, 'xxx', [{'a1': 'a'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'],)
|
|
b = make_pr(repo_b, 'xxx', [{'b1': 'b'}], user=config['role_user']['token'], reviewer=config['role_reviewer']['token'], statuses=[])
|
|
env.run_crons()
|
|
|
|
pr_a = to_pr(env, a)
|
|
assert pr_a.blocked
|
|
|
|
with repo_a: a.post_comment('hansen skipchecks', config['role_reviewer']['token'])
|
|
assert not pr_a.blocked
|
|
pr_a.skipchecks = False
|
|
|
|
with repo_b: b.post_comment('hansen skipchecks', config['role_reviewer']['token'])
|
|
assert not pr_a.blocked
|
|
|
|
def test_different_branches(env, project, repo_a, repo_b, config):
|
|
project.write({
|
|
'branch_ids': [(0, 0, {'name': 'dev'})]
|
|
})
|
|
# repo_b only works with master
|
|
env['runbot_merge.repository'].search([('name', '=', repo_b.name)])\
|
|
.branch_filter = '[("name", "=", "master")]'
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': '0'}), ref='heads/dev')
|
|
repo_a.make_commits(None, Commit('initial', tree={'b': '0'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b': '0'}), ref='heads/master')
|
|
|
|
pr_a = make_pr(
|
|
repo_a, 'xxx', [{'a': '1'}],
|
|
target='dev',
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token']
|
|
)
|
|
env.run_crons()
|
|
|
|
with repo_a:
|
|
pr_a.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
repo_a.post_status('heads/staging.dev', 'success')
|
|
env.run_crons()
|
|
|
|
assert to_pr(env, pr_a).state == 'merged'
|
|
|
|
def test_remove_acl(env, partners, repo_a, repo_b, repo_c):
|
|
""" Check that our way of deprovisioning works correctly
|
|
"""
|
|
r = partners['self_reviewer']
|
|
assert r.mapped('review_rights.repository_id.name') == [repo_a.name, repo_b.name, repo_c.name]
|
|
r.write({'review_rights': [(5, 0, 0)]})
|
|
assert r.mapped('review_rights.repository_id') == env['runbot_merge.repository']
|
|
|
|
class TestSubstitutions:
|
|
def test_substitution_patterns(self, env, port):
|
|
p = env['runbot_merge.project'].create({
|
|
'name': 'proj',
|
|
'github_token': 'wheeee',
|
|
'repo_ids': [(0, 0, {'name': 'xxx/xxx'})],
|
|
'branch_ids': [(0, 0, {'name': 'master'})]
|
|
})
|
|
env['runbot_merge.events_sources'].create({'repository': 'xxx/xxx'})
|
|
r = p.repo_ids
|
|
# replacement pattern, pr label, stored label
|
|
cases = [
|
|
('/^foo:/foo-dev:/', 'foo:bar', 'foo-dev:bar'),
|
|
('/^foo:/foo-dev:/', 'foox:bar', 'foox:bar'),
|
|
('/^foo:/foo-dev:/i', 'FOO:bar', 'foo-dev:bar'),
|
|
('/o/x/g', 'foo:bar', 'fxx:bar'),
|
|
('@foo:@bar:@', 'foo:bar', 'bar:bar'),
|
|
('/foo:/bar:/\n/bar:/baz:/', 'foo:bar', 'baz:bar'),
|
|
]
|
|
for pr_number, (pattern, original, target) in enumerate(cases, start=1):
|
|
r.substitutions = pattern
|
|
requests.post(
|
|
'http://localhost:{}/runbot_merge/hooks'.format(port),
|
|
headers={'X-Github-Event': 'pull_request'},
|
|
json={
|
|
'action': 'opened',
|
|
'repository': {
|
|
'full_name': r.name,
|
|
},
|
|
'pull_request': {
|
|
'state': 'open',
|
|
'draft': False,
|
|
'user': {'login': 'bob'},
|
|
'base': {
|
|
'repo': {'full_name': r.name},
|
|
'ref': p.branch_ids.name,
|
|
},
|
|
'number': pr_number,
|
|
'title': "a pr",
|
|
'body': None,
|
|
'commits': 1,
|
|
'head': {
|
|
'label': original,
|
|
'sha': format(pr_number, 'x')*40,
|
|
}
|
|
},
|
|
'sender': {'login': 'pytest'}
|
|
}
|
|
)
|
|
pr = env['runbot_merge.pull_requests'].search([
|
|
('repository', '=', r.id),
|
|
('number', '=', pr_number)
|
|
])
|
|
assert pr.label == target
|
|
|
|
|
|
def test_substitutions_staging(self, env, repo_a, repo_b, config):
|
|
""" Different repos from the same project may have different policies for
|
|
sourcing PRs. So allow for remapping labels on input in order to match.
|
|
"""
|
|
repo_b_id = env['runbot_merge.repository'].search([
|
|
('name', '=', repo_b.name)
|
|
])
|
|
# in repo b, replace owner part by repo_a's owner
|
|
repo_b_id.substitutions = r"/.+:/%s:/" % repo_a.owner
|
|
|
|
with repo_a:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': '0'}), ref='heads/master')
|
|
with repo_b:
|
|
repo_b.make_commits(None, Commit('initial', tree={'b': '0'}), ref='heads/master')
|
|
|
|
# policy is that repo_a PRs are created in the same repo while repo_b PRs
|
|
# are created in personal forks
|
|
with repo_a:
|
|
repo_a.make_commits('master', repo_a.Commit('bop', tree={'a': '1'}), ref='heads/abranch')
|
|
pra = repo_a.make_pr(target='master', head='abranch')
|
|
with repo_b, repo_b.fork() as b_fork:
|
|
b_fork.make_commits('master', b_fork.Commit('pob', tree={'b': '1'}), ref='heads/abranch')
|
|
prb = repo_b.make_pr(
|
|
title="a pr",
|
|
target='master', head='%s:abranch' % b_fork.owner
|
|
)
|
|
|
|
pra_id = to_pr(env, pra)
|
|
prb_id = to_pr(env, prb)
|
|
assert pra_id.label.endswith(':abranch')
|
|
assert prb_id.label.endswith(':abranch')
|
|
|
|
with repo_a, repo_b:
|
|
repo_a.post_status(pra.head, 'success')
|
|
pra.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
|
|
repo_b.post_status(prb.head, 'success')
|
|
prb.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
|
|
assert pra_id.staging_id, 'PR A should be staged'
|
|
assert prb_id.staging_id, "PR B should be staged"
|
|
assert pra_id.staging_id == prb_id.staging_id, "both prs should be staged together"
|
|
assert pra_id.batch_id == prb_id.batch_id, "both prs should be part of the same batch"
|
|
|
|
def test_multi_project(env, make_repo, setreviewers, users, config,
|
|
tunnel):
|
|
""" There should be no linking of PRs across projects, even if there is some
|
|
structural overlap between the two.
|
|
|
|
Here we have two projects on different forks, then a user creates a PR from
|
|
a third fork (or one of the forks should not matter) to *both*.
|
|
|
|
The two PRs should be independent.
|
|
"""
|
|
Projects = env['runbot_merge.project']
|
|
gh_token = config['github']['token']
|
|
|
|
r1 = make_repo("repo_a")
|
|
with r1:
|
|
r1.make_commits(
|
|
None, Commit('root', tree={'a': 'a'}),
|
|
ref='heads/default')
|
|
r1_dev = r1.fork()
|
|
p1 = Projects.create({
|
|
'name': 'Project 1',
|
|
'github_token': gh_token,
|
|
'github_prefix': 'hansen',
|
|
'repo_ids': [(0, 0, {
|
|
'name': r1.name,
|
|
'group_id': False,
|
|
'required_statuses': 'a',
|
|
})],
|
|
'branch_ids': [(0, 0, {'name': 'default'})],
|
|
})
|
|
setreviewers(*p1.repo_ids)
|
|
env['runbot_merge.events_sources'].create([{'repository': r1.name}])
|
|
|
|
r2 = make_repo('repo_b')
|
|
with r2:
|
|
r2.make_commits(
|
|
None, Commit('root', tree={'b': 'a'}),
|
|
ref='heads/default'
|
|
)
|
|
r2_dev = r2.fork()
|
|
p2 = Projects.create({
|
|
'name': "Project 2",
|
|
'github_token': gh_token,
|
|
'github_prefix': 'hansen',
|
|
'repo_ids': [(0, 0, {
|
|
'name': r2.name,
|
|
'group_id': False,
|
|
'required_statuses': 'a',
|
|
})],
|
|
'branch_ids': [(0, 0, {'name': 'default'})],
|
|
})
|
|
setreviewers(*p2.repo_ids)
|
|
env['runbot_merge.events_sources'].create([{'repository': r2.name}])
|
|
|
|
assert r1_dev.owner == r2_dev.owner
|
|
|
|
with r1, r1_dev:
|
|
r1_dev.make_commits('default', Commit('new', tree={'a': 'b'}), ref='heads/other')
|
|
|
|
# create, validate, and approve pr1
|
|
pr1 = r1.make_pr(title='pr 1', target='default', head=r1_dev.owner + ':other')
|
|
r1.post_status(pr1.head, 'success', 'a')
|
|
pr1.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
|
|
with r2, r2_dev:
|
|
r2_dev.make_commits('default', Commit('new', tree={'b': 'b'}), ref='heads/other')
|
|
|
|
# create second PR with the same label *in a different project*, don't
|
|
# approve it
|
|
pr2 = r2.make_pr(title='pr 2', target='default', head=r2_dev.owner + ':other')
|
|
r2.post_status(pr2.head, 'success', 'a')
|
|
env.run_crons()
|
|
|
|
pr1_id = to_pr(env, pr1)
|
|
pr2_id = to_pr(env, pr2)
|
|
|
|
assert pr1_id.state == 'ready' and not pr1_id.blocked
|
|
assert pr2_id.state == 'validated'
|
|
|
|
assert pr1_id.staging_id
|
|
assert not pr2_id.staging_id
|
|
|
|
assert pr1.comments == [
|
|
(users['reviewer'], 'hansen r+'),
|
|
seen(env, pr1, users),
|
|
]
|
|
assert pr2.comments == [seen(env, pr2, users)]
|
|
|
|
def test_freeze_complete(env, project, repo_a, repo_b, repo_c, users, config):
|
|
""" Tests the freeze wizard feature (aside from the UI):
|
|
|
|
* have a project with 3 repos, and two branches (1.0 and master) each
|
|
* have 2 PRs required for the freeze
|
|
* prep 3 freeze PRs
|
|
* prep 1 bump PR
|
|
* trigger the freeze wizard
|
|
* trigger it again (check that the same object is returned, there should
|
|
only be one freeze per project at a time)
|
|
* configure the freeze
|
|
* check that it doesn't go through
|
|
* merge required PRs
|
|
* check that freeze goes through
|
|
* check that reminder is shown
|
|
* check that new branches are created w/ correct parent & commit info
|
|
* check that a PRs (freeze and bump) are part of synthetic stagings so
|
|
they're correctly accounted for in the change history
|
|
"""
|
|
project.freeze_reminder = "Don't forget to like and subscribe"
|
|
|
|
# have a project with 3 repos, and two branches (1.0 and master)
|
|
project.branch_ids = [
|
|
(1, project.branch_ids.id, {'sequence': 1}),
|
|
(0, 0, {'name': '1.0', 'sequence': 2}),
|
|
]
|
|
|
|
[
|
|
(master_head_a, master_head_b, master_head_c),
|
|
(pr_required_a, _, pr_required_c),
|
|
(pr_rel_a, pr_rel_b, pr_rel_c),
|
|
pr_bump_a,
|
|
pr_other
|
|
] = setup_mess(repo_a, repo_b, repo_c)
|
|
env.run_crons() # process the PRs
|
|
|
|
release_prs = {
|
|
repo_a.name: to_pr(env, pr_rel_a),
|
|
repo_b.name: to_pr(env, pr_rel_b),
|
|
repo_c.name: to_pr(env, pr_rel_c),
|
|
}
|
|
pr_bump_id = to_pr(env, pr_bump_a)
|
|
# trigger the ~~tree~~ freeze wizard
|
|
w = project.action_prepare_freeze()
|
|
w2 = project.action_prepare_freeze()
|
|
assert w == w2, "each project should only have one freeze wizard active at a time"
|
|
assert w['res_model'] == 'runbot_merge.project.freeze'
|
|
|
|
w_id = env[w['res_model']].browse([w['res_id']])
|
|
assert w_id.branch_name == '1.1', "check that the forking incremented the minor by 1"
|
|
assert len(w_id.release_pr_ids) == len(project.repo_ids), \
|
|
"should ask for a many release PRs as we have repositories"
|
|
|
|
# configure required PRs
|
|
w_id.required_pr_ids = (to_pr(env, pr_required_a) | to_pr(env, pr_required_c)).ids
|
|
# configure releases
|
|
for r in w_id.release_pr_ids:
|
|
r.pr_id = release_prs[r.repository_id.name].id
|
|
w_id.release_pr_ids[-1].pr_id = to_pr(env, pr_other).id
|
|
# configure bump
|
|
assert not w_id.bump_pr_ids, "there is no bump pr by default"
|
|
w_id.write({'bump_pr_ids': [
|
|
(0, 0, {'repository_id': pr_bump_id.repository.id, 'pr_id': pr_bump_id.id})
|
|
]})
|
|
r = w_id.action_freeze()
|
|
assert r == w, "the freeze is not ready so the wizard should redirect to itself"
|
|
assert w_id.errors == f"""\
|
|
* All release PRs must have the same label, found '{pr_rel_c.user}:release-1.1, {pr_other.user}:whocares'.
|
|
* 2 required PRs not ready."""
|
|
w_id.release_pr_ids[-1].pr_id = release_prs[repo_c.name].id
|
|
|
|
with repo_a:
|
|
pr_required_a.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
repo_a.post_status(pr_required_a.head, 'success')
|
|
with repo_c:
|
|
pr_required_c.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
repo_c.post_status(pr_required_c.head, 'success')
|
|
env.run_crons()
|
|
|
|
for repo in [repo_a, repo_b, repo_c]:
|
|
with repo:
|
|
repo.post_status('staging.master', 'success')
|
|
env.run_crons()
|
|
|
|
assert to_pr(env, pr_required_a).state == 'merged'
|
|
assert to_pr(env, pr_required_c).state == 'merged'
|
|
|
|
assert not w_id.errors
|
|
|
|
# assume the wizard is closed, re-open it
|
|
w = project.action_prepare_freeze()
|
|
assert w['res_model'] == 'runbot_merge.project.freeze'
|
|
assert w['res_id'] == w_id.id, "check that we're still getting the old wizard"
|
|
w_id = env[w['res_model']].browse([w['res_id']])
|
|
assert w_id.exists()
|
|
|
|
# actually perform the freeze
|
|
r = w_id.action_freeze()
|
|
# check that the wizard was deleted
|
|
assert not w_id.exists()
|
|
# check that the wizard pops out a reminder dialog (kinda)
|
|
assert r['res_model'] == 'runbot_merge.project'
|
|
assert r['res_id'] == project.id
|
|
|
|
release_pr_ids = functools.reduce(operator.add, release_prs.values())
|
|
# stuff that's done directly
|
|
assert all(pr_id.state == 'merged' for pr_id in release_pr_ids)
|
|
assert pr_bump_id.state == 'merged'
|
|
assert pr_bump_id.commits_map != '{}'
|
|
|
|
assert len(release_pr_ids.batch_id) == 1
|
|
assert release_pr_ids.batch_id.merge_date
|
|
assert release_pr_ids.batch_id.staging_ids.target.name == '1.1'
|
|
assert release_pr_ids.batch_id.staging_ids.state == 'success'
|
|
|
|
assert pr_bump_id.batch_id.merge_date
|
|
assert pr_bump_id.batch_id.staging_ids.target.name == 'master'
|
|
assert pr_bump_id.batch_id.staging_ids.state == 'success'
|
|
|
|
# stuff that's behind a cron
|
|
env.run_crons()
|
|
|
|
# check again to be sure
|
|
assert all(pr_id.state == 'merged' for pr_id in release_pr_ids)
|
|
assert pr_bump_id.state == 'merged'
|
|
|
|
assert pr_rel_a.state == "closed"
|
|
assert pr_rel_a.base['ref'] == '1.1'
|
|
assert pr_rel_b.state == "closed"
|
|
assert pr_rel_b.base['ref'] == '1.1'
|
|
assert pr_rel_c.state == "closed"
|
|
assert pr_rel_c.base['ref'] == '1.1'
|
|
assert all(pr_id.target.name == '1.1' for pr_id in release_pr_ids)
|
|
|
|
assert pr_bump_a.state == 'closed'
|
|
assert pr_bump_a.base['ref'] == 'master'
|
|
assert pr_bump_id.target.name == 'master'
|
|
|
|
m_a = repo_a.commit('master')
|
|
assert m_a.message.startswith('Bump A')
|
|
assert repo_a.read_tree(m_a) == {
|
|
'f': '1', # from master
|
|
'g': 'x', # from required PR (merged into master before forking)
|
|
'version': '1.2-alpha', # from bump PR
|
|
}
|
|
|
|
c_a = repo_a.commit('1.1')
|
|
assert c_a.message.startswith('Release 1.1 (A)')
|
|
assert repo_a.read_tree(c_a) == {
|
|
'f': '1', # from master
|
|
'g': 'x', # from required pr
|
|
'version': '1.1', # from release commit
|
|
}
|
|
c_a_parent = repo_a.commit(c_a.parents[0])
|
|
assert c_a_parent.message.startswith('super important file')
|
|
assert c_a_parent.parents[0] == master_head_a
|
|
|
|
c_b = repo_b.commit('1.1')
|
|
assert c_b.message.startswith('Release 1.1 (B)')
|
|
assert repo_b.read_tree(c_b) == {'f': '1', 'version': '1.1'}
|
|
assert c_b.parents[0] == master_head_b
|
|
|
|
c_c = repo_c.commit('1.1')
|
|
assert c_c.message.startswith('Release 1.1 (C)')
|
|
assert repo_c.read_tree(c_c) == {'f': '2', 'version': '1.1'}
|
|
assert repo_c.commit(c_c.parents[0]).parents[0] == master_head_c
|
|
|
|
|
|
def setup_mess(repo_a, repo_b, repo_c):
|
|
master_heads = []
|
|
for r in [repo_a, repo_b, repo_c]:
|
|
with r:
|
|
[root, _] = r.make_commits(
|
|
None,
|
|
Commit('base', tree={'version': '', 'f': '0'}),
|
|
Commit('release 1.0', tree={'version': '1.0'}),
|
|
ref='heads/1.0'
|
|
)
|
|
master_heads.extend(r.make_commits(root, Commit('other', tree={'f': '1'}), ref='heads/master'))
|
|
|
|
a_fork = repo_a.fork()
|
|
b_fork = repo_b.fork()
|
|
c_fork = repo_c.fork()
|
|
assert a_fork.owner == b_fork.owner == c_fork.owner
|
|
owner = a_fork.owner
|
|
# have 2 PRs required for the freeze
|
|
with repo_a, a_fork:
|
|
a_fork.make_commits(master_heads[0], Commit('super important file', tree={'g': 'x'}), ref='heads/apr')
|
|
pr_required_a = repo_a.make_pr(target='master', head=f'{owner}:apr', title="xxx")
|
|
with repo_c, c_fork:
|
|
c_fork.make_commits(master_heads[2], Commit('update thing', tree={'f': '2'}), ref='heads/cpr')
|
|
pr_required_c = repo_c.make_pr(target='master', head=f'{owner}:cpr', title="yyy")
|
|
# have 3 release PRs, only the first one updates the tree (version file)
|
|
with repo_a, a_fork:
|
|
a_fork.make_commits(
|
|
master_heads[0],
|
|
Commit('Release 1.1 (A)', tree={'version': '1.1'}),
|
|
ref='heads/release-1.1'
|
|
)
|
|
pr_rel_a = repo_a.make_pr(target='master', head=f'{owner}:release-1.1', title="zzz")
|
|
with repo_b, b_fork:
|
|
b_fork.make_commits(
|
|
master_heads[1],
|
|
Commit('Release 1.1 (B)', tree={'version': '1.1'}),
|
|
ref='heads/release-1.1'
|
|
)
|
|
pr_rel_b = repo_b.make_pr(target='master', head=f'{owner}:release-1.1', title="000")
|
|
with repo_c, c_fork:
|
|
c_fork.make_commits(master_heads[2], Commit("Some change", tree={'a': '1'}), ref='heads/whocares')
|
|
pr_other = repo_c.make_pr(target='master', head=f'{owner}:whocares', title="111")
|
|
c_fork.make_commits(
|
|
master_heads[2],
|
|
Commit('Release 1.1 (C)', tree={'version': '1.1'}),
|
|
ref='heads/release-1.1'
|
|
)
|
|
pr_rel_c = repo_c.make_pr(target='master', head=f'{owner}:release-1.1', title="222")
|
|
# have one bump PR on repo A
|
|
with repo_a, a_fork:
|
|
a_fork.make_commits(
|
|
master_heads[0],
|
|
Commit("Bump A", tree={'version': '1.2-alpha'}),
|
|
ref='heads/bump-1.1',
|
|
)
|
|
pr_bump_a = repo_a.make_pr(target='master', head=f'{owner}:bump-1.1', title="333")
|
|
return master_heads, (pr_required_a, None, pr_required_c), (pr_rel_a, pr_rel_b, pr_rel_c), pr_bump_a, pr_other
|
|
|
|
def test_freeze_subset(env, project, repo_a, repo_b, repo_c, users, config):
|
|
"""It should be possible to only freeze a subset of a project when e.g. one
|
|
of the repository is managed differently than the rest and has
|
|
non-synchronous releases.
|
|
|
|
- it should be possible to mark repositories as non-freezed (just opted out
|
|
of the entire thing), in which case no freeze PRs should be asked of them
|
|
- it should be possible to remove repositories from the freeze wizard
|
|
- repositories which are not in the freeze wizard should just not be frozen
|
|
|
|
To do things correctly that should probably match with the branch filters
|
|
and stuff, but that's a configuration concern.
|
|
"""
|
|
# have a project with 3 repos, and two branches (1.0 and master)
|
|
project.branch_ids = [
|
|
(1, project.branch_ids.id, {'sequence': 1}),
|
|
(0, 0, {'name': '1.0', 'sequence': 2}),
|
|
]
|
|
|
|
masters = []
|
|
for r in [repo_a, repo_b, repo_c]:
|
|
with r:
|
|
[root, _] = r.make_commits(
|
|
None,
|
|
Commit('base', tree={'version': '', 'f': '0'}),
|
|
Commit('release 1.0', tree={'version': '1.0'} if r is repo_a else None),
|
|
ref='heads/1.0'
|
|
)
|
|
masters.extend(r.make_commits(root, Commit('other', tree={'f': '1'}), ref='heads/master'))
|
|
|
|
with repo_a:
|
|
repo_a.make_commits(
|
|
masters[0],
|
|
Commit('Release 1.1', tree={'version': '1.1'}),
|
|
ref='heads/release-1.1'
|
|
)
|
|
pr_rel_a = repo_a.make_pr(target='master', head='release-1.1')
|
|
|
|
# the third repository we opt out of freezing
|
|
project.repo_ids.filtered(lambda r: r.name == repo_c.name).freeze = False
|
|
env.run_crons() # process the PRs
|
|
|
|
# open the freeze wizard
|
|
w = project.action_prepare_freeze()
|
|
w_id = env[w['res_model']].browse([w['res_id']])
|
|
# check that there are only rels for repos A and B
|
|
assert w_id.mapped('release_pr_ids.repository_id.name') == [repo_a.name, repo_b.name]
|
|
# remove B from the set
|
|
b_id = w_id.release_pr_ids.filtered(lambda r: r.repository_id.name == repo_b.name)
|
|
w_id.write({'release_pr_ids': [(3, b_id.id, 0)]})
|
|
assert len(w_id.release_pr_ids) == 1
|
|
# set lone release PR
|
|
w_id.release_pr_ids.pr_id = to_pr(env, pr_rel_a).id
|
|
assert not w_id.errors
|
|
|
|
w_id.action_freeze()
|
|
assert not w_id.exists()
|
|
|
|
assert repo_a.commit('1.1'), "should have created branch in repo A"
|
|
try:
|
|
repo_b.commit('1.1')
|
|
pytest.fail("should *not* have created branch in repo B")
|
|
except AssertionError:
|
|
...
|
|
try:
|
|
repo_c.commit('1.1')
|
|
pytest.fail("should *not* have created branch in repo C")
|
|
except AssertionError:
|
|
...
|
|
# can't stage because we (wilfully) don't have branches 1.1 in repos B and C
|
|
|
|
@pytest.mark.skip("env's session is not thread-safe sadface")
|
|
def test_race_conditions():
|
|
"""need the ability to dup the env in order to send concurrent requests to
|
|
the inner odoo
|
|
|
|
- try to run the action_freeze during a cron (merge or staging), should
|
|
error (recover and return nice message?)
|
|
- somehow get ahead of the action and update master's commit between moment
|
|
where it is fetched and moment where the bump pr is fast-forwarded,
|
|
there's actually a bit of time thanks to the rate limiting (fetch of base,
|
|
update of tmp to base, rebase of commits on tmp, wait 1s, for each release
|
|
and bump PR, then the release branches are created, and finally the bump
|
|
prs)
|
|
"""
|
|
...
|
|
|
|
def test_freeze_conflict(env, project, repo_a, repo_b, repo_c, users, config):
|
|
"""If one of the branches we're trying to create already exists, the wizard
|
|
fails.
|
|
"""
|
|
project.branch_ids = [
|
|
(1, project.branch_ids.id, {'sequence': 1}),
|
|
(0, 0, {'name': '1.0', 'sequence': 2}),
|
|
]
|
|
heads, _, (pr_rel_a, pr_rel_b, pr_rel_c), bump, other = \
|
|
setup_mess(repo_a, repo_b, repo_c)
|
|
env.run_crons()
|
|
|
|
release_prs = {
|
|
repo_a.name: to_pr(env, pr_rel_a),
|
|
repo_b.name: to_pr(env, pr_rel_b),
|
|
repo_c.name: to_pr(env, pr_rel_c),
|
|
}
|
|
|
|
w = project.action_prepare_freeze()
|
|
w_id = env[w['res_model']].browse([w['res_id']])
|
|
for repo, release_pr in release_prs.items():
|
|
w_id.release_pr_ids\
|
|
.filtered(lambda r: r.repository_id.name == repo)\
|
|
.pr_id = release_pr.id
|
|
|
|
# create conflicting branch
|
|
with repo_c:
|
|
[c] = repo_c.make_commits(heads[2], Commit("exists", tree={'version': ''}))
|
|
repo_c.make_ref('heads/1.1', c)
|
|
|
|
# actually perform the freeze
|
|
with pytest.raises(xmlrpc.client.Fault) as e:
|
|
w_id.action_freeze()
|
|
assert f"Unable to create branch {repo_c.name}:1.1" in e.value.faultString
|
|
|
|
# branches a and b should have been deleted
|
|
with pytest.raises(AssertionError) as e:
|
|
repo_a.get_ref('heads/1.1')
|
|
assert e.value.args[0].startswith("Not Found")
|
|
with pytest.raises(AssertionError) as e:
|
|
repo_b.get_ref('heads/1.1')
|
|
assert e.value.args[0].startswith("Not Found")
|
|
|
|
def test_cancel_staging(env, project, repo_a, repo_b, users, config):
|
|
"""If a batch is flagged as staging cancelling (from any PR), the staging
|
|
should get cancelled if and when the batch transitions to unblocked
|
|
"""
|
|
with repo_a, repo_b:
|
|
repo_a.make_commits(None, Commit('initial', tree={'a': '1'}), ref='heads/master')
|
|
repo_b.make_commits(None, Commit('initial', tree={'b': '1'}), ref='heads/master')
|
|
|
|
pr_a = make_pr(repo_a, 'batch', [{'a': '2'}], user=config['role_user']['token'], statuses=[], reviewer=None)
|
|
pr_b = make_pr(repo_b, 'batch', [{'b': '2'}], user=config['role_user']['token'], statuses=[], reviewer=None)
|
|
pr_lone = make_pr(
|
|
repo_a,
|
|
"C",
|
|
[{'c': '1'}],
|
|
user=config['role_user']['token'],
|
|
reviewer=config['role_reviewer']['token'],
|
|
)
|
|
env.run_crons()
|
|
|
|
a_id, b_id, lone_id = map(to_pr, repeat(env), [pr_a, pr_b, pr_lone])
|
|
assert lone_id.staging_id
|
|
st = lone_id.staging_id
|
|
|
|
with repo_a:
|
|
pr_a.post_comment("hansen cancel=staging", config['role_reviewer']['token'])
|
|
assert a_id.state == 'opened'
|
|
assert a_id.cancel_staging
|
|
assert b_id.cancel_staging
|
|
assert lone_id.staging_id == st
|
|
with repo_a:
|
|
pr_a.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
assert a_id.state == 'approved'
|
|
assert lone_id.staging_id == st
|
|
with repo_a:
|
|
repo_a.post_status(a_id.head, 'success')
|
|
env.run_crons()
|
|
assert a_id.state == 'ready'
|
|
assert lone_id.staging_id == st
|
|
|
|
assert b_id.state == 'opened'
|
|
with repo_b:
|
|
pr_b.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
assert b_id.state == 'approved'
|
|
assert lone_id.staging_id == st
|
|
with repo_b:
|
|
repo_b.post_status(b_id.head, 'success')
|
|
assert b_id.state == 'approved'
|
|
assert lone_id.staging_id == st
|
|
env.run_crons()
|
|
assert b_id.state == 'ready'
|
|
# should have cancelled the staging, picked a and b, and re-staged the
|
|
# entire thing
|
|
assert lone_id.staging_id != st
|
|
|
|
assert len({
|
|
lone_id.staging_id.id,
|
|
a_id.staging_id.id,
|
|
b_id.staging_id.id,
|
|
}) == 1
|