runbot/runbot_merge/tests/test_multirepo.py
Xavier Morel 679d556c90 [FIX] project creation: handling of mergebot info
- don't *fail* in `_compute_identity`, it causes issues when the token
  is valid but doesn't have `user:email` access as the request is
  aborted and saving doesn't work
- make `github_name` and `github_email` required rather than ad-hoc
  requiring them in `_compute_identity` (which doesn't work correctly)
- force copy of `github_name` and `github_email`, with o2ms being
  !copy this means duplicating projects now works out of the box (or
  should...)

Currently errors in `_compute_identity` are reported via logging which
is not great as it's not UI visible, should probably get moved to
chatter eventually but that's not currently enabled on projects.

Fixes #990
2024-12-02 16:32:53 +01:00

1547 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 not p.blocked
assert not p.staging_id
with repo_a: pr.post_comment('hansen rebase-merge', config['role_reviewer']['token'])
env.run_crons()
assert p.staging_id
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',
'github_name': "yyy",
'github_email': "zzz@example.org",
'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',
'github_name': config['github']['name'],
'github_email': "foo@example.org",
'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',
'github_name': config['github']['name'],
'github_email': "foo@example.org",
'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