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

Broken (can't run odoo at all): - In Odoo 17.0, the `pre_init_hook` takes an env, not a cursor, update `_check_citext`. - Odoo 17.0 rejects `@attrs` and doesn't say where they are or how to update them, fun, hunt down `attrs={'invisible': ...` and try to fix them. - Odoo 17.0 warns on non-multi creates, update them, most were very reasonable, one very wasn't. Test failures: - Odoo 17.0 deprecates `name_get` and doesn't use it as a *source* anymore, replace overrides by overrides to `_compute_display_name`. - Multiple tracking changes: - `_track_set_author` takes a `Partner` not an id. - `_message_compute_author` still requires overriding in order to handle record creation, which in standard doesn't support author overriding. - `mail.tracking.value.field_type` has been removed, the field type now needs to be retrieved from the `field_id`. - Some tracking ordering have changed and require adjusting a few tests. Also added a few flushes before SQL queries which are not (obviously at least) at the start of a cron or controller, no test failure observed but better safe than sorry (probably).
366 lines
13 KiB
Python
366 lines
13 KiB
Python
from operator import itemgetter
|
|
|
|
import requests
|
|
|
|
from utils import Commit, to_pr, seen
|
|
|
|
|
|
def test_partner_merge(env):
|
|
p_src = env['res.partner'].create({
|
|
'name': "xxx",
|
|
'github_login': 'xxx'
|
|
})
|
|
# proper login with useful info
|
|
p_dest = env['res.partner'].create({
|
|
'name': 'Partner P. Partnersson',
|
|
'github_login': ''
|
|
})
|
|
|
|
env['base.partner.merge.automatic.wizard'].create({
|
|
'state': 'selection',
|
|
'partner_ids': (p_src + p_dest).ids,
|
|
'dst_partner_id': p_dest.id,
|
|
}).action_merge()
|
|
assert not p_src.exists()
|
|
assert p_dest.name == 'Partner P. Partnersson'
|
|
assert p_dest.github_login == 'xxx'
|
|
|
|
def test_name_search(env):
|
|
""" PRs should be findable by:
|
|
|
|
* number
|
|
* display_name (`repository#number`)
|
|
* label
|
|
|
|
This way we can find parents or sources by these informations.
|
|
"""
|
|
p = env['runbot_merge.project'].create({
|
|
'name': 'proj',
|
|
'github_token': 'no',
|
|
})
|
|
b = env['runbot_merge.branch'].create({
|
|
'name': 'target',
|
|
'project_id': p.id
|
|
})
|
|
r = env['runbot_merge.repository'].create({
|
|
'name': 'repo',
|
|
'project_id': p.id,
|
|
})
|
|
|
|
baseline = {'target': b.id, 'repository': r.id}
|
|
PRs = env['runbot_merge.pull_requests']
|
|
prs = PRs.create({**baseline, 'number': 1964, 'label': 'victor:thump', 'head': 'a', 'message': 'x'})\
|
|
| PRs.create({**baseline, 'number': 1959, 'label': 'marcus:frankenstein', 'head': 'b', 'message': 'y'})\
|
|
| PRs.create({**baseline, 'number': 1969, 'label': 'victor:patch-1', 'head': 'c', 'message': 'z'})
|
|
pr0, pr1, pr2 = [[pr.id, pr.display_name] for pr in prs]
|
|
|
|
assert PRs.name_search('1964') == [pr0]
|
|
assert PRs.name_search('1969') == [pr2]
|
|
|
|
assert PRs.name_search('frank') == [pr1]
|
|
assert PRs.name_search('victor') == [pr2, pr0]
|
|
|
|
assert PRs.name_search('thump') == [pr0]
|
|
|
|
assert PRs.name_search('repo') == [pr2, pr0, pr1]
|
|
assert PRs.name_search('repo#1959') == [pr1]
|
|
|
|
def test_unreviewer(env, project, port):
|
|
repo = env['runbot_merge.repository'].create({
|
|
'project_id': project.id,
|
|
'name': 'a_test_repo',
|
|
'status_ids': [(0, 0, {'context': 'status'})]
|
|
})
|
|
p = env['res.partner'].create({
|
|
'name': 'George Pearce',
|
|
'github_login': 'emubitch',
|
|
'review_rights': [(0, 0, {'repository_id': repo.id, 'review': True})]
|
|
})
|
|
|
|
r = requests.post(f'http://localhost:{port}/runbot_merge/get_reviewers', json={
|
|
'jsonrpc': '2.0',
|
|
'id': None,
|
|
'method': 'call',
|
|
'params': {},
|
|
})
|
|
r.raise_for_status()
|
|
assert 'error' not in r.json()
|
|
assert r.json()['result'] == ['emubitch']
|
|
|
|
r = requests.post(f'http://localhost:{port}/runbot_merge/remove_reviewers', json={
|
|
'jsonrpc': '2.0',
|
|
'id': None,
|
|
'method': 'call',
|
|
'params': {'github_logins': ['emubitch']},
|
|
})
|
|
r.raise_for_status()
|
|
assert 'error' not in r.json()
|
|
|
|
assert p.review_rights == env['res.partner.review']
|
|
|
|
def test_staging_post_update(env, repo, users, config):
|
|
"""Because statuses come from commits, it's possible to update the commits
|
|
of a staging after that staging has completed (one way or the other), either
|
|
by sending statuses directly (e.g. rebuilding, for non-deterministic errors)
|
|
or just using the staging's head commit in a branch.
|
|
|
|
This makes post-mortem analysis quite confusing, so stagings should
|
|
"lock in" their statuses once they complete.
|
|
"""
|
|
|
|
with repo:
|
|
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref='heads/master')
|
|
|
|
repo.make_commits(m, Commit('thing', tree={'m': 'c'}), ref='heads/other')
|
|
pr = repo.make_pr(target='master', head='other')
|
|
repo.post_status(pr.head, 'success')
|
|
pr.post_comment('hansen r+ rebase-merge', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
pr_id = to_pr(env, pr)
|
|
staging_id = pr_id.staging_id
|
|
assert staging_id
|
|
|
|
staging_head = repo.commit('staging.master')
|
|
with repo:
|
|
repo.post_status(staging_head, 'failure')
|
|
env.run_crons()
|
|
assert pr_id.state == 'error'
|
|
assert staging_id.state == 'failure'
|
|
assert staging_id.statuses == [
|
|
[repo.name, 'default', 'failure', ''],
|
|
]
|
|
|
|
with repo:
|
|
repo.post_status(staging_head, 'success')
|
|
env.run_crons()
|
|
assert staging_id.state == 'failure'
|
|
assert staging_id.statuses == [
|
|
[repo.name, 'default', 'failure', ''],
|
|
]
|
|
|
|
def test_merge_empty_commits(env, repo, users, config):
|
|
"""The mergebot should allow merging already-empty commits.
|
|
"""
|
|
with repo:
|
|
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref='heads/master')
|
|
|
|
repo.make_commits(m, Commit('thing1', tree={}), ref='heads/other1')
|
|
pr1 = repo.make_pr(target='master', head='other1')
|
|
repo.post_status(pr1.head, 'success')
|
|
pr1.post_comment('hansen r+', config['role_reviewer']['token'])
|
|
|
|
repo.make_commits(m, Commit('thing2', tree={}), ref='heads/other2')
|
|
pr2 = repo.make_pr(target='master', head='other2')
|
|
repo.post_status(pr2.head, 'success')
|
|
pr2.post_comment('hansen r+ rebase-ff', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
pr1_id = to_pr(env, pr1)
|
|
pr2_id = to_pr(env, pr2)
|
|
assert pr1_id.staging_id and pr2_id.staging_id
|
|
|
|
with repo:
|
|
repo.post_status('staging.master', 'success')
|
|
env.run_crons()
|
|
|
|
assert pr1_id.state == pr2_id.state == 'merged'
|
|
|
|
# log is most-recent-first (?)
|
|
commits = list(repo.log('master'))
|
|
head = repo.commit(commits[0]['sha'])
|
|
assert repo.read_tree(head) == {'m': 'm'}
|
|
|
|
assert commits[0]['commit']['message'].startswith('thing2')
|
|
assert commits[1]['commit']['message'].startswith('thing1')
|
|
assert commits[2]['commit']['message'] == 'initial'
|
|
|
|
|
|
def test_merge_emptying_commits(env, repo, users, config):
|
|
"""The mergebot should *not* allow merging non-empty commits which become
|
|
empty as part of the staging (rebasing)
|
|
"""
|
|
with repo:
|
|
[m, _] = repo.make_commits(
|
|
None,
|
|
Commit('initial', tree={'m': 'm'}),
|
|
Commit('second', tree={'m': 'c'}),
|
|
ref='heads/master',
|
|
)
|
|
|
|
[c1] = repo.make_commits(m, Commit('thing', tree={'m': 'c'}), ref='heads/branch1')
|
|
pr1 = repo.make_pr(target='master', head='branch1')
|
|
repo.post_status(pr1.head, 'success')
|
|
pr1.post_comment('hansen r+ rebase-ff', config['role_reviewer']['token'])
|
|
|
|
[_, c2] = repo.make_commits(
|
|
m,
|
|
Commit('thing1', tree={'c': 'c'}),
|
|
Commit('thing2', tree={'m': 'c'}),
|
|
ref='heads/branch2',
|
|
)
|
|
pr2 = repo.make_pr(target='master', head='branch2')
|
|
repo.post_status(pr2.head, 'success')
|
|
pr2.post_comment('hansen r+ rebase-ff', config['role_reviewer']['token'])
|
|
|
|
repo.make_commits(
|
|
m,
|
|
Commit('thing1', tree={'m': 'x'}),
|
|
Commit('thing2', tree={'m': 'c'}),
|
|
ref='heads/branch3',
|
|
)
|
|
pr3 = repo.make_pr(target='master', head='branch3')
|
|
repo.post_status(pr3.head, 'success')
|
|
pr3.post_comment('hansen r+ squash', config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
|
|
ping = f"@{users['user']} @{users['reviewer']}"
|
|
# check that first / sole commit emptying is caught
|
|
pr1_id = to_pr(env, pr1)
|
|
assert not pr1_id.staging_id
|
|
assert pr1.comments[3:] == [
|
|
(users['user'], f"{ping} unable to stage: commit {c1} results in an empty tree when merged, it is likely a duplicate of a merged commit, rebase and remove.")
|
|
]
|
|
assert pr1_id.error
|
|
assert pr1_id.state == 'error'
|
|
|
|
# check that followup commit emptying is caught
|
|
pr2_id = to_pr(env, pr2)
|
|
assert not pr2_id.staging_id
|
|
assert pr2.comments[3:] == [
|
|
(users['user'], f"{ping} unable to stage: commit {c2} results in an empty tree when merged, it is likely a duplicate of a merged commit, rebase and remove.")
|
|
]
|
|
assert pr2_id.error
|
|
assert pr2_id.state == 'error'
|
|
|
|
# check that emptied squashed pr is caught
|
|
pr3_id = to_pr(env, pr3)
|
|
assert not pr3_id.staging_id
|
|
assert pr3.comments[3:] == [
|
|
(users['user'], f"{ping} unable to stage: results in an empty tree when merged, might be the duplicate of a merged PR.")
|
|
]
|
|
assert pr3_id.error
|
|
assert pr3_id.state == 'error'
|
|
|
|
# ensure the PR does not get re-staged since it's the first of the staging
|
|
# (it's the only one)
|
|
env.run_crons()
|
|
assert pr1.comments[3:] == [
|
|
(users['user'], f"{ping} unable to stage: commit {c1} results in an empty tree when merged, it is likely a duplicate of a merged commit, rebase and remove.")
|
|
]
|
|
assert len(pr2.comments) == 4
|
|
assert len(pr3.comments) == 4
|
|
|
|
def test_force_ready(env, repo, config):
|
|
with repo:
|
|
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref="heads/master")
|
|
|
|
repo.make_commits(m, Commit('first', tree={'m': 'c1'}), ref="heads/other")
|
|
pr = repo.make_pr(target='master', head='other')
|
|
env.run_crons()
|
|
|
|
pr_id = to_pr(env, pr)
|
|
pr_id.skipchecks = True
|
|
|
|
assert pr_id.state == 'ready'
|
|
assert pr_id.status == 'pending'
|
|
reviewer = env['res.users'].browse([env._uid]).partner_id
|
|
assert pr_id.reviewed_by == reviewer
|
|
|
|
def test_help(env, repo, config, users, partners):
|
|
with repo:
|
|
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref="heads/master")
|
|
|
|
repo.make_commits(m, Commit('first', tree={'m': 'c1'}), ref="heads/other")
|
|
pr = repo.make_pr(target='master', head='other')
|
|
env.run_crons()
|
|
|
|
for role in ['reviewer', 'self_reviewer', 'user', 'other']:
|
|
v = config[f'role_{role}']
|
|
with repo:
|
|
pr.post_comment("hansen help", v['token'])
|
|
with repo:
|
|
pr.post_comment("hansen r+ help", config['role_reviewer']['token'])
|
|
|
|
assert not partners['reviewer'].user_ids, "the reviewer should not be an internal user"
|
|
|
|
group_internal = env.ref("base.group_user")
|
|
group_admin = env.ref("runbot_merge.group_admin")
|
|
env['res.users'].create({
|
|
'partner_id': partners['reviewer'].id,
|
|
'login': 'reviewer',
|
|
'groups_id': [(4, group_internal.id, 0), (4, group_admin.id, 0)],
|
|
})
|
|
|
|
with repo:
|
|
pr.post_comment("hansen help", config['role_reviewer']['token'])
|
|
env.run_crons()
|
|
|
|
assert pr.comments == [
|
|
seen(env, pr, users),
|
|
(users['reviewer'], "hansen help"),
|
|
(users['self_reviewer'], "hansen help"),
|
|
(users['user'], "hansen help"),
|
|
(users['other'], "hansen help"),
|
|
(users['reviewer'], "hansen r+ help"),
|
|
(users['reviewer'], "hansen help"),
|
|
(users['user'], REVIEWER.format(user=users['reviewer'], skip="")),
|
|
(users['user'], RANDO.format(user=users['self_reviewer'])),
|
|
(users['user'], AUTHOR.format(user=users['user'])),
|
|
(users['user'], RANDO.format(user=users['other'])),
|
|
(users['user'],
|
|
REVIEWER.format(user=users['reviewer'], skip='')
|
|
+ "\n\nWarning: in invoking help, every other command has been ignored."),
|
|
(users['user'], REVIEWER.format(
|
|
user=users['reviewer'],
|
|
skip='|`skipchecks`|bypasses both statuses and review|\n',
|
|
)),
|
|
]
|
|
|
|
REVIEWER = """\
|
|
Currently available commands for @{user}:
|
|
|
|
|command||
|
|
|-|-|
|
|
|`help`|displays this help|
|
|
|`r(eview)+`|approves the PR, if it's a forwardport also approves all non-detached parents|
|
|
|`r(eview)=<number>`|only approves the specified parents|
|
|
|`fw=no`|does not forward-port this PR|
|
|
|`fw=default`|forward-ports this PR normally|
|
|
|`fw=skipci`|does not wait for a forward-port's statuses to succeed before creating the next one|
|
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
|
|`merge`|integrate the PR with a simple merge commit, using the PR description as message|
|
|
|`rebase-merge`|rebases the PR on top of the target branch the integrates with a merge commit, using the PR description as message|
|
|
|`rebase-ff`|rebases the PR on top of the target branch, then fast-forwards|
|
|
|`squash`|squashes the PR as a single commit on the target branch, using the PR description as message|
|
|
|`delegate+`|grants approval rights to the PR author|
|
|
|`delegate=<...>`|grants approval rights on this PR to the specified github users|
|
|
|`default`|stages the PR normally|
|
|
|`priority`|tries to stage this PR first, then adds `default` PRs if the staging has room|
|
|
|`alone`|stages this PR only with other PRs of the same priority|
|
|
{skip}\
|
|
|`cancel=staging`|automatically cancels the current staging when this PR becomes ready|
|
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
|
|
|
Note: this help text is dynamic and will change with the state of the PR.\
|
|
"""
|
|
AUTHOR = """\
|
|
Currently available commands for @{user}:
|
|
|
|
|command||
|
|
|-|-|
|
|
|`help`|displays this help|
|
|
|`fw=no`|does not forward-port this PR|
|
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
|
|
|
Note: this help text is dynamic and will change with the state of the PR.\
|
|
"""
|
|
RANDO = """\
|
|
Currently available commands for @{user}:
|
|
|
|
|command||
|
|
|-|-|
|
|
|`help`|displays this help|
|
|
|
|
Note: this help text is dynamic and will change with the state of the PR.\
|
|
"""
|