Skip to content

Commit 61fedcb

Browse files
authored
Merge pull request #54 from srfraser/verification2
Bug 1470232 Improving version bump safety
2 parents f2f78cb + 1ff70b3 commit 61fedcb

File tree

7 files changed

+79
-12
lines changed

7 files changed

+79
-12
lines changed

treescript/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ def __init__(self, msg):
2929
super(FailedSubprocess, self).__init__(
3030
msg, exit_code=STATUSES['internal-error']
3131
)
32+
33+
34+
class ChangesetMismatchError(ScriptWorkerTaskException):
35+
"""An incorrect number of changesets would be pushed."""
36+
37+
def __init__(self, msg):
38+
"""Initialize ChangesetMismatchError.
39+
40+
Args:
41+
msg (str): the reason for throwing an exception.
42+
"""
43+
super(ChangesetMismatchError, self).__init__(
44+
msg, exit_code=STATUSES['internal-error']
45+
)

treescript/mercurial.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55

66
from treescript.utils import execute_subprocess
7-
from treescript.exceptions import FailedSubprocess
7+
from treescript.exceptions import FailedSubprocess, ChangesetMismatchError
88
from treescript.task import get_source_repo, get_tag_info
99

1010
# https://www.mercurial-scm.org/repo/hg/file/tip/tests/run-tests.py#l1040
@@ -90,7 +90,7 @@ async def run_hg_command(context, *args, local_repo=None):
9090
env = build_hg_environment()
9191
if local_repo:
9292
command.extend(['-R', local_repo])
93-
await execute_subprocess(command, env=env)
93+
return await execute_subprocess(command, env=env)
9494

9595

9696
# log_mercurial_version {{{1
@@ -207,6 +207,23 @@ async def log_outgoing(context, directory):
207207
await run_hg_command(context, 'out', '-vp', '-r', '.', dest_repo, local_repo=local_repo)
208208

209209

210+
async def assert_outgoing(context, directory, expected_cset_count):
211+
"""Run `hg out` to ensure the expected number of changes exist.
212+
213+
Assuming that each action produces one changeset, we can
214+
check to see if we'd push to the right location by comparing
215+
the results of 'hg out' - if it's not equal to the number
216+
of actions, we have unexpected changes to push.
217+
"""
218+
local_repo = os.path.join(directory, 'src')
219+
dest_repo = get_source_repo(context.task)
220+
log.info("outgoing changesets..")
221+
changesets = await run_hg_command(context, 'out', '-q', '-r', '.', dest_repo, local_repo=local_repo)
222+
if len(changesets) != expected_cset_count:
223+
raise ChangesetMismatchError(
224+
'Expected {} changesets to push, found {}'.format(expected_cset_count, changesets))
225+
226+
210227
async def push(context):
211228
"""Run `hg push` against the current source repo."""
212229
local_repo = context.repo

treescript/script.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from scriptworker.exceptions import ScriptWorkerException
99
from treescript.utils import task_action_types, is_dry_run
1010
from treescript.mercurial import log_mercurial_version, validate_robustcheckout_works, \
11-
checkout_repo, do_tagging, log_outgoing, push
11+
checkout_repo, do_tagging, log_outgoing, assert_outgoing, push
1212
from treescript.versionmanip import bump_version
1313

1414
log = logging.getLogger(__name__)
@@ -29,6 +29,8 @@ async def do_actions(context, actions, directory):
2929
else:
3030
raise NotImplementedError("Unexpected action")
3131
await log_outgoing(context, directory)
32+
# 'push' doesn't generate a changeset, the other actions do.
33+
await assert_outgoing(context, directory, len(actions)-actions.count('push'))
3234
if is_dry_run(context.task):
3335
log.info("Not pushing changes, dry_run was forced")
3436
elif 'push' in actions:

treescript/test/test_mercurial.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212
from scriptworker.context import Context
1313
from treescript import mercurial
14-
from treescript.exceptions import FailedSubprocess
14+
from treescript.exceptions import FailedSubprocess, ChangesetMismatchError
1515
from treescript.script import get_default_config
1616
from treescript.utils import mkdir
1717
from treescript.test import tmpdir, noop_async, is_slice_in_list
@@ -162,7 +162,8 @@ async def check_params(*args, **kwargs):
162162
args)
163163

164164
mocker.patch.object(mercurial, 'run_hg_command', new=check_params)
165-
mocker.patch.object(mercurial, 'get_source_repo').return_value = "https://hg.mozilla.org/test-repo"
165+
mocker.patch.object(
166+
mercurial, 'get_source_repo').return_value = "https://hg.mozilla.org/test-repo"
166167

167168
context.config['hg_share_base_dir'] = '/builds/hg-shared-test'
168169
context.config['upstream_repo'] = 'https://hg.mozilla.org/mozilla-test-unified'
@@ -255,3 +256,30 @@ async def run_command(context, *arguments, local_repo=None):
255256
assert is_slice_in_list(('out', '-vp'), called_args[0][0])
256257
assert is_slice_in_list(('-r', '.'), called_args[0][0])
257258
assert is_slice_in_list(('https://hg.mozilla.org/treescript-test', ), called_args[0][0])
259+
260+
261+
@pytest.mark.asyncio
262+
async def test_assert_outgoing(context, mocker):
263+
fake_csets = ['cset1', 'cset2']
264+
265+
async def dummy_run_hg_command(*args, **kwargs):
266+
return fake_csets
267+
mocker.patch.object(mercurial, 'run_hg_command', new=dummy_run_hg_command)
268+
269+
mocked_source_repo = mocker.patch.object(mercurial, 'get_source_repo')
270+
mocked_source_repo.return_value = 'https://hg.mozilla.org/treescript-test'
271+
await mercurial.assert_outgoing(context, context.config['work_dir'], len(fake_csets))
272+
273+
274+
@pytest.mark.asyncio
275+
async def test_assert_outgoing_failure(context, mocker):
276+
fake_csets = ['cset1', 'cset2']
277+
278+
async def dummy_run_hg_command(*args, **kwargs):
279+
return fake_csets + ['cset3']
280+
mocker.patch.object(mercurial, 'run_hg_command', new=dummy_run_hg_command)
281+
282+
mocked_source_repo = mocker.patch.object(mercurial, 'get_source_repo')
283+
mocked_source_repo.return_value = 'https://hg.mozilla.org/treescript-test'
284+
with pytest.raises(ChangesetMismatchError):
285+
await mercurial.assert_outgoing(context, context.config['work_dir'], len(fake_csets))

treescript/test/test_script.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ async def mocked_push(*args, **kwargs):
109109
mocker.patch.object(script, 'bump_version', new=mocked_bump)
110110
mocker.patch.object(script, 'push', new=mocked_push)
111111
mocker.patch.object(script, 'log_outgoing', new=noop_async)
112+
mocker.patch.object(script, 'assert_outgoing', new=noop_async)
112113
mocker.patch.object(script, 'is_dry_run').return_value = dry_run
113114
await script.do_actions(context, actions, directory='/some/folder/here')
114115
assert called_tag[0]

treescript/test/test_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def test_is_dry_run_true():
8585
assert True is utils.is_dry_run(task)
8686

8787

88-
# log_output {{{1
88+
# process_output {{{1
8989
@pytest.mark.asyncio
90-
async def test_log_output(tmpdir, mocker):
90+
async def test_process_output(tmpdir, mocker):
9191
logged = []
9292
with open(__file__, 'r') as fh:
9393
contents = fh.read()
@@ -113,7 +113,7 @@ async def __anext__(self):
113113
mockfh = mock.MagicMock()
114114
aiter = AsyncIterator()
115115
mockfh.readline = aiter.__anext__
116-
await utils.log_output(mockfh)
116+
await utils.process_output(mockfh)
117117
assert contents.rstrip() == '\n'.join(logged)
118118

119119

treescript/utils.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,24 @@ def is_dry_run(task):
7878
return dry_run
7979

8080

81-
# log_output {{{1
82-
async def log_output(fh):
81+
# process_output {{{1
82+
async def process_output(fh):
8383
"""Log the output from an async generator.
8484
8585
Args:
8686
fh (async generator): the async generator to log output from
8787
8888
"""
89+
output = []
8990
while True:
9091
line = await fh.readline()
9192
if line:
92-
log.info(line.decode("utf-8").rstrip())
93+
line = line.decode("utf-8").rstrip()
94+
log.info(line)
95+
output.append(line)
9396
else:
9497
break
98+
return output
9599

96100

97101
# execute_subprocess {{{1
@@ -114,9 +118,10 @@ async def execute_subprocess(command, **kwargs):
114118
*command, stdout=PIPE, stderr=STDOUT, **kwargs
115119
)
116120
log.info("COMMAND OUTPUT: ")
117-
await log_output(subprocess.stdout)
121+
output = await process_output(subprocess.stdout)
118122
exitcode = await subprocess.wait()
119123
log.info("exitcode {}".format(exitcode))
120124

121125
if exitcode != 0:
122126
raise FailedSubprocess('Command `{}` failed'.format(' '.join(command)))
127+
return output

0 commit comments

Comments
 (0)