diff --git a/docs/setup/error_codes.md b/docs/setup/error_codes.md index 14b5e4b..3eb72a8 100644 --- a/docs/setup/error_codes.md +++ b/docs/setup/error_codes.md @@ -64,7 +64,10 @@ provided, error code 4000 will be used by default. _\**kwargs_ can be any number of key/value pairs that correspond with string format fields in the error message. -`raise error.GlazierError(code[int], exception[str], collect[bool], **kwargs)` +```python +raise G(exception: Optional[str], + replacements: Optional[List[Union[bool, int, str]]]) +``` **Example file, let's say division.py**: @@ -76,16 +79,27 @@ b = 0 try: a / b except ZeroDivisionError as e: - raise error.GlazierError(4101, e, true, num1=a, num2=b) + raise GDivideByZeroError(e, [a, b]) ``` **Example corresponding message defined in error.py**: ```python -errors: dict[int, str] = { - 4000: 'Uncaught exception', - 4101: 'Failed to divide {} by {}', # <-- This is the added error message with num1 and num2 kwargs - 5000: 'Failed to reach web server', - 5300: 'Service unavailable', - } +# This is the added error message with a and b args +GDivideByZeroError = _new_err(4101, 'Failed to divide {} by {}') +``` + +The error message will be as follows: + +> Failed to divide 1 by 0 + +Once this exception is caught by `autobuild.py`, the following log message will +be displayed: + +``` +Failed to divide 1 by 0 + +Exception: division by zero + +Need help? Visit https://glazier-failures.example.com#4101 ``` diff --git a/glazier/lib/error.py b/glazier/lib/error.py index 823bd91..9dffe10 100644 --- a/glazier/lib/error.py +++ b/glazier/lib/error.py @@ -12,98 +12,92 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Standarize logging for all errors.""" +"""Create custom error class and centrally define all errors.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging -import os -import sys -from typing import Any, Optional - -from glazier.lib import buildinfo -from glazier.lib import constants -from glazier.lib import logs - -_SUFFIX = (f'Need help? Visit {constants.HELP_URI}') - -build_info = buildinfo.BuildInfo() - - -def get_message(code: int, **kwargs) -> str: - """Return dict value of a given error code. - - Args: - code: Error code to return message for. - **kwargs: Key/value pairs used in string.format() replacements in the error - message. - - Returns: - Message associated to the error code. - - Raises: - GlazierError: Failed to determine error message from code - """ - # TODO: Investigate how to gaurentee unique error codes - errors: dict[int, str] = { - 1337: 'Reserved {}', - 4000: 'Uncaught exception', - 4301: 'Failed to determine error message from code', - 4302: 'Failed to collect logs', - 5000: 'Failed to reach web server', - 5300: 'Service unavailable', - } - - error_msg = errors.get(code) - - if not error_msg: - raise GlazierError(4301) - - # Enable passing variables to the errors dict by optionally inserting values - # associated with the keyword arguments via string.format(). - return str(error_msg.format(*kwargs.values())) - - -def _collect(): - """Collect failure logs into a zip file.""" - try: - logs.Collect( - os.path.join(build_info.CachePath() + os.sep, 'glazier_logs.zip')) - except logs.LogError as e: - raise GlazierError(4302, e, False) +from typing import List, Optional, Type, Union class GlazierError(Exception): - """Custom exception class for Glazier errors.""" + """Creates an error object. + + This error object can be interacted with via it's attributes directly. For + example, the following code can be used to determine the attributes of this + class: + + try: + raise GlazierError('SomeException', [ + 'SomeReplacement', 'SomeOtherReplacement']) + except GlazierError as e: + for att in dir(e): + print(att, getattr(e, att)) + + The default code if all fails is 4000. This acts as a fallback for when there + is an unknown exception. + + Attributes: + code: Error code with an associated message + message: Error message string with an associated code + exception: Exception message string + replacements: Any number of values that the error message should contain + """ def __init__(self, - code: int = 4000, - exception: Optional[Any] = None, - collect: bool = True, - **kwargs): - """Log a terminating failure. + exception: Optional[str] = '', + replacements: Optional[List[Union[bool, int, str]]] = None): + self.code = 4000 + self.message = '' + self.exception = exception + self.replacements = replacements + super().__init__(exception, replacements) - Args: - code: Error code to append to the failure message. - exception: Exception message string. - collect: Whether to collect log files. - **kwargs: Key/value pairs of any number of string replacements in the - error message. - """ - super(GlazierError, self).__init__() + def __str__(self) -> str: + string = '' - msg = f'{get_message(code, **kwargs)}\n\n' + if self.message and self.replacements: + # Asterisk is used to unpack the values of the list + string = f'{self.message.format(*self.replacements)} ' + elif self.message: + string = f'{self.message} ' + + string += f'({self.code})' # TODO: Add exception file and lineno. - if exception: - msg += f'Exception: {exception}\n\n' + if self.exception: + string += f': {self.exception}' - msg += f'{_SUFFIX}#{code}' + return string - if collect: - _collect() - logging.critical(msg) - sys.exit(1) # Necessary to avoid traceback +def _new_err(code: int, message: str) -> Type[GlazierError]: + """Captures code and message pairs for every error. + + This method acts to store the error codes and the associated messages to be + passed to the GlazierException class. + + Args: + code: Error code with an associated message + message: Error message string with an associated code + + Returns: + GlazierError exception with all required attributes. + """ + + class Error(GlazierError): + + def __init__(self, + exception: Optional[str] = '', + replacements: Optional[List[Union[bool, int, str]]] = None): + super().__init__(exception, replacements) + self.code: int = code + self.message: str = message + + return Error + +################################################################################ +# ERROR CODES (https://google.github.io/glazier/setup/error_codes) # +################################################################################ +GReservedError = _new_err(1337, 'Reserved {} {} {}') +GUncaughtError = _new_err(4000, 'Uncaught exception') +GCollectLogsError = _new_err(4301, 'Failed to collect logs') +GWebServerError = _new_err(5000, 'Failed to reach web server') +GServiceError = _new_err(5300, 'Service unavailable') diff --git a/glazier/lib/error_test.py b/glazier/lib/error_test.py index 4c6f44f..f45cdd5 100644 --- a/glazier/lib/error_test.py +++ b/glazier/lib/error_test.py @@ -14,59 +14,25 @@ # limitations under the License. """Tests for glazier.lib.errors.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from absl.testing import absltest from glazier.lib import error -import mock -from pyfakefs import fake_filesystem - -_SUFFIX_WITH_CODE = ( - f'Need help? Visit {error.constants.HELP_URI}#1337') class ErrorsTest(absltest.TestCase): - def setUp(self): - super(ErrorsTest, self).setUp() - self.fs = fake_filesystem.FakeFilesystem() - fake_filesystem.FakeOsModule(self.fs) - fake_filesystem.FakeFileOpen(self.fs) - self.fs.create_file( - error.os.path.join(error.build_info.CachePath() + error.os.sep, - 'glazier_logs.zip')) + def test_glazier_reserved_error_replacements(self): + self.assertEqual( + str(error.GReservedError('exception', [1, 2, 3])), + 'Reserved 1 2 3 (1337): exception') - @mock.patch.object(error.logs, 'Collect', autospec=True) - def test_collect(self, collect): - error._collect() - self.assertTrue(collect.called) - - @mock.patch.object(error.logs, 'Collect', autospec=True) - def test_collect_failure(self, collect): - collect.side_effect = error.logs.LogError('something went wrong') - with self.assertRaises(SystemExit): - error._collect() - - @mock.patch.object(error, '_collect', autospec=True) - @mock.patch.object(error.logging, 'critical', autospec=True) - def test_glazier_error_default(self, crit, collect): - with self.assertRaises(SystemExit): - error.GlazierError() - self.assertTrue(crit.called) - self.assertTrue(collect.called) - - @mock.patch.object(error.logs, 'Collect', autospec=True) - @mock.patch.object(error.logging, 'critical', autospec=True) - def test_glazier_error_custom(self, crit, collect): - collect.return_value = 0 - with self.assertRaises(SystemExit): - error.GlazierError(1337, 'The exception', False, a='code') - crit.assert_called_with( - f'Reserved code\n\nException: The exception\n\n{_SUFFIX_WITH_CODE}') + def test_glazier_reserved_error_no_replacements(self): + self.assertEqual( + str(error.GUncaughtError('exception')), + 'Uncaught exception (4000): exception') + def test_glazier_error_str(self): + self.assertEqual(str(error.GlazierError('exception')), '(4000): exception') if __name__ == '__main__': absltest.main()