Parameterize the Options class so we can synthesize an unhandled exception

This commit is contained in:
Jean-Paul Calderone 2020-12-16 21:15:24 -05:00
parent 2746eb9ae1
commit a04a915628
3 changed files with 44 additions and 21 deletions

View File

@ -106,8 +106,7 @@ def parse_options(argv, config=None):
config.parseOptions(argv) # may raise usage.error config.parseOptions(argv) # may raise usage.error
return config return config
def parse_or_exit_with_explanation(argv, stdout=sys.stdout): def parse_or_exit_with_explanation_with_config(config, argv, stdout, stderr):
config = Options()
try: try:
parse_options(argv[1:], config=config) parse_options(argv[1:], config=config)
except usage.error as e: except usage.error as e:
@ -171,7 +170,7 @@ def _maybe_enable_eliot_logging(options, reactor):
# Pass on the options so we can dispatch the subcommand. # Pass on the options so we can dispatch the subcommand.
return options return options
def run(argv=sys.argv, stderr=sys.stderr): def run(configFactory=Options, argv=sys.argv, stdout=sys.stdout, stderr=sys.stderr):
# TODO(3035): Remove tox-check when error becomes a warning # TODO(3035): Remove tox-check when error becomes a warning
if 'TOX_ENV_NAME' not in os.environ: if 'TOX_ENV_NAME' not in os.environ:
assert sys.version_info < (3,), u"Tahoe-LAFS does not run under Python 3. Please use Python 2.7.x." assert sys.version_info < (3,), u"Tahoe-LAFS does not run under Python 3. Please use Python 2.7.x."
@ -180,7 +179,15 @@ def run(argv=sys.argv, stderr=sys.stderr):
from allmydata.windows.fixups import initialize from allmydata.windows.fixups import initialize
initialize() initialize()
# doesn't return: calls sys.exit(rc) # doesn't return: calls sys.exit(rc)
task.react(lambda reactor: _run_with_reactor(reactor, argv, stderr)) task.react(
lambda reactor: _run_with_reactor(
reactor,
configFactory(),
argv,
stdout,
stderr,
),
)
def _setup_coverage(reactor, argv): def _setup_coverage(reactor, argv):
@ -223,13 +230,19 @@ def _setup_coverage(reactor, argv):
reactor.addSystemEventTrigger('after', 'shutdown', write_coverage_data) reactor.addSystemEventTrigger('after', 'shutdown', write_coverage_data)
def _run_with_reactor(reactor, argv, stderr): def _run_with_reactor(reactor, config, argv, stdout, stderr):
_setup_coverage(reactor, argv) _setup_coverage(reactor, argv)
d = defer.maybeDeferred(parse_or_exit_with_explanation, argv) d = defer.maybeDeferred(
parse_or_exit_with_explanation_with_config,
config,
argv,
stdout,
stderr,
)
d.addCallback(_maybe_enable_eliot_logging, reactor) d.addCallback(_maybe_enable_eliot_logging, reactor)
d.addCallback(dispatch) d.addCallback(dispatch, stdout=stdout, stderr=stderr)
def _show_exception(f): def _show_exception(f):
# when task.react() notices a non-SystemExit exception, it does # when task.react() notices a non-SystemExit exception, it does
# log.err() with the failure and then exits with rc=1. We want this # log.err() with the failure and then exits with rc=1. We want this

View File

@ -510,14 +510,13 @@ class CLI(CLITestMixin, unittest.TestCase):
def test_exception_catcher(self): def test_exception_catcher(self):
self.basedir = "cli/exception_catcher" self.basedir = "cli/exception_catcher"
stderr = StringIO()
exc = Exception("canary") exc = Exception("canary")
ns = Namespace() class BrokenOptions(object):
def parseOptions(self, argv):
raise exc
ns.parse_called = False stderr = StringIO()
def call_parse_or_exit(args): ns = Namespace()
ns.parse_called = True
raise exc
ns.sys_exit_called = False ns.sys_exit_called = False
def call_sys_exit(exitcode): def call_sys_exit(exitcode):
@ -531,19 +530,17 @@ class CLI(CLITestMixin, unittest.TestCase):
# it's safe to drop it on the floor. # it's safe to drop it on the floor.
f(reactor) f(reactor)
patcher = MonkeyPatcher((runner, 'parse_or_exit_with_explanation', patcher = MonkeyPatcher((sys, 'exit', call_sys_exit),
call_parse_or_exit),
(sys, 'exit', call_sys_exit),
(task, 'react', fake_react), (task, 'react', fake_react),
) )
patcher.runWithPatches( patcher.runWithPatches(
lambda: runner.run( lambda: runner.run(
configFactory=BrokenOptions,
argv=["tahoe"], argv=["tahoe"],
stderr=stderr, stderr=stderr,
), ),
) )
self.failUnless(ns.parse_called)
self.failUnless(ns.sys_exit_called) self.failUnless(ns.sys_exit_called)
self.failUnlessIn(str(exc), stderr.getvalue()) self.failUnlessIn(str(exc), stderr.getvalue())

View File

@ -3,6 +3,9 @@ from __future__ import print_function
import os import os
import time import time
import signal import signal
from functools import (
partial,
)
from random import randrange from random import randrange
from six.moves import StringIO from six.moves import StringIO
from io import ( from io import (
@ -100,10 +103,20 @@ def run_cli_bytes(verb, *args, **kwargs):
stdout = TextIOWrapper(BytesIO(), encoding) stdout = TextIOWrapper(BytesIO(), encoding)
stderr = TextIOWrapper(BytesIO(), encoding) stderr = TextIOWrapper(BytesIO(), encoding)
d = defer.succeed(argv) d = defer.succeed(argv)
d.addCallback(runner.parse_or_exit_with_explanation, stdout=stdout) d.addCallback(
d.addCallback(runner.dispatch, partial(
stdin=StringIO(stdin), runner.parse_or_exit_with_explanation_with_config,
stdout=stdout, stderr=stderr) runner.Options(),
),
stdout=stdout,
stderr=stderr,
)
d.addCallback(
runner.dispatch,
stdin=StringIO(stdin),
stdout=stdout,
stderr=stderr,
)
def _done(rc): def _done(rc):
return 0, _getvalue(stdout), _getvalue(stderr) return 0, _getvalue(stdout), _getvalue(stderr)
def _err(f): def _err(f):