From 13913fd8e0650c289ae7369a6afe0f0c10ee4228 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 30 Jun 2015 12:57:51 -0700 Subject: [PATCH] Added traceback dump --- .gitignore | 1 + letsencrypt/cli.py | 39 +++++++++++++++++++----- letsencrypt/tests/cli_test.py | 57 +++++++++++++++++++---------------- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 9564cfbc7..54670e6da 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ dist/ /venv/ /.tox/ +letsencrypt.log # coverage .coverage diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8f833522d..f2d2c4ef0 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -645,18 +645,43 @@ def _setup_logging(args): def _handle_exception(exc_type, exc_value, trace, args): + """Logs exceptions and reports them to the user. + + Args is used to determine how to display exceptions to the user. In + general, if args.debug is True, then the full exception and traceback is + shown to the user, otherwise it is suppressed. If args itself is None, + then the traceback and exception is attempted to be written to a logfile. + If this is successful, the traceback is suppressed, otherwise it is shown + to the user. sys.exit is always called with a nonzero status. + + """ logger.debug( "Exiting abnormally:\n%s", "".join(traceback.format_exception(exc_type, exc_value, trace))) - if issubclass(exc_type, errors.Error) and (not args or not args.debug): - sys.exit(exc_value) - elif issubclass(exc_type, Exception) and args and not args.debug: - sys.exit( - "An unexpected error occurred. Please see the logfiles in {0} for " - "more details.".format(args.logs_dir)) + if issubclass(exc_type, Exception) and (args is None or not args.debug): + if args is None: + try: + with open("letsencrypt.log", "w") as logfile: + traceback.print_exception( + exc_type, exc_value, trace, file=logfile) + except: # pylint: disable=bare-except + sys.exit("".join( + traceback.format_exception(exc_type, exc_value, trace))) + + if issubclass(exc_type, errors.Error): + sys.exit(exc_value) + elif args is None: + sys.exit( + "An unexpected error occurred. Please see the logfile '{0}' " + "for more details.".format(os.path.abspath("letsencrypt.log"))) + else: + sys.exit( + "An unexpected error occurred. Please see the logfiles in {0} " + "for more details.".format(args.logs_dir)) else: - traceback.print_exception(exc_type, exc_value, trace, file=sys.stderr) + sys.exit("".join( + traceback.format_exception(exc_type, exc_value, trace))) def main(cli_args=sys.argv[1:]): diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 8da009864..cdc5826df 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -2,11 +2,14 @@ import itertools import os import shutil +import traceback import tempfile import unittest import mock +from letsencrypt import errors + class CLITest(unittest.TestCase): """Tests for different commands.""" @@ -20,16 +23,13 @@ class CLITest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tmp_dir) - def _call(self, args, client_mock_attrs=None): + def _call(self, args): from letsencrypt import cli args = ['--text', '--config-dir', self.config_dir, '--work-dir', self.work_dir, '--logs-dir', self.logs_dir] + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: - if client_mock_attrs: - # pylint: disable=star-args - client.configure_mock(**client_mock_attrs) ret = cli.main(args) return ret, stdout, stderr, client @@ -59,36 +59,41 @@ class CLITest(unittest.TestCase): for r in xrange(len(flags)))): self._call(['plugins',] + list(args)) - def test_exceptions(self): - from letsencrypt import errors - cmd_arg = ['config_changes'] - error = [errors.Error('problem')] - attrs = {'view_config_changes.side_effect' : error} - self.assertRaises( - errors.Error, self._call, ['--debug'] + cmd_arg, attrs) - - attrs['view_config_changes.side_effect'] = [ValueError] - self.assertRaises( - ValueError, self._call, ['--debug'] + cmd_arg, attrs) - @mock.patch("letsencrypt.cli.sys") def test_handle_exception(self, mock_sys): # pylint: disable=protected-access - import StringIO from letsencrypt import cli - from letsencrypt import errors - cli._handle_exception(errors.Error, "detail", None, None) - mock_sys.exit.assert_called_once_with("detail") + mock_open = mock.mock_open() + with mock.patch("letsencrypt.cli.open", mock_open, create=True): + exception = Exception("detail") + cli._handle_exception( + Exception, exc_value=exception, trace=None, args=None) + mock_open().write.assert_called_once_with("".join( + traceback.format_exception_only(Exception, exception))) + error_msg = mock_sys.exit.call_args_list[0][0][0] + self.assertTrue("unexpected error" in error_msg) + + with mock.patch("letsencrypt.cli.open", mock_open, create=True): + mock_open.side_effect = [KeyboardInterrupt] + error = errors.Error("detail") + cli._handle_exception( + errors.Error, exc_value=error, trace=None, args=None) + # assert_any_call used because sys.exit doesn't exit in cli.py + mock_sys.exit.assert_any_call("".join( + traceback.format_exception_only(errors.Error, error))) args = mock.MagicMock(debug=False) - cli._handle_exception(ValueError, "detail", None, args) - self.assertTrue("logfile" in mock_sys.exit.call_args_list[1][0][0]) + cli._handle_exception( + Exception, exc_value=Exception("detail"), trace=None, args=args) + error_msg = mock_sys.exit.call_args_list[-1][0][0] + self.assertTrue("unexpected error" in error_msg) - mock_sys.stderr = StringIO.StringIO() - exc_value = "A very specific string" - cli._handle_exception(KeyboardInterrupt, exc_value, None, None) - self.assertTrue(exc_value in mock_sys.stderr.getvalue()) + interrupt = KeyboardInterrupt("detail") + cli._handle_exception( + KeyboardInterrupt, exc_value=interrupt, trace=None, args=None) + mock_sys.exit.assert_called_with("".join( + traceback.format_exception_only(KeyboardInterrupt, interrupt))) if __name__ == '__main__':