""" 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 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