Introduce GroupedExceptionHandler and use it to simplify bootstrap error handling (#30192)
This commit is contained in:
parent
f40f1b5c7c
commit
a681fd7b42
@ -11,7 +11,9 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from six import string_types
|
from six import string_types
|
||||||
@ -1009,3 +1011,64 @@ def __repr__(self):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.data)
|
return str(self.data)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupedExceptionHandler(object):
|
||||||
|
"""A generic mechanism to coalesce multiple exceptions and preserve tracebacks."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.exceptions = [] # type: List[Tuple[str, Exception, List[str]]]
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
"""Whether any exceptions were handled."""
|
||||||
|
return bool(self.exceptions)
|
||||||
|
|
||||||
|
def forward(self, context):
|
||||||
|
# type: (str) -> GroupedExceptionForwarder
|
||||||
|
"""Return a contextmanager which extracts tracebacks and prefixes a message."""
|
||||||
|
return GroupedExceptionForwarder(context, self)
|
||||||
|
|
||||||
|
def _receive_forwarded(self, context, exc, tb):
|
||||||
|
# type: (str, Exception, List[str]) -> None
|
||||||
|
self.exceptions.append((context, exc, tb))
|
||||||
|
|
||||||
|
def grouped_message(self, with_tracebacks=True):
|
||||||
|
# type: (bool) -> str
|
||||||
|
"""Print out an error message coalescing all the forwarded errors."""
|
||||||
|
each_exception_message = [
|
||||||
|
'{0} raised {1}: {2}{3}'.format(
|
||||||
|
context,
|
||||||
|
exc.__class__.__name__,
|
||||||
|
exc,
|
||||||
|
'\n{0}'.format(''.join(tb)) if with_tracebacks else '',
|
||||||
|
)
|
||||||
|
for context, exc, tb in self.exceptions
|
||||||
|
]
|
||||||
|
return 'due to the following failures:\n{0}'.format(
|
||||||
|
'\n'.join(each_exception_message)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupedExceptionForwarder(object):
|
||||||
|
"""A contextmanager to capture exceptions and forward them to a
|
||||||
|
GroupedExceptionHandler."""
|
||||||
|
|
||||||
|
def __init__(self, context, handler):
|
||||||
|
# type: (str, GroupedExceptionHandler) -> None
|
||||||
|
self._context = context
|
||||||
|
self._handler = handler
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
if exc_value is not None:
|
||||||
|
self._handler._receive_forwarded(
|
||||||
|
self._context,
|
||||||
|
exc_value,
|
||||||
|
traceback.format_tb(tb),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Suppress any exception from being re-raised:
|
||||||
|
# https://docs.python.org/3/reference/datamodel.html#object.__exit__.
|
||||||
|
return True
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import llnl.util.filesystem as fs
|
import llnl.util.filesystem as fs
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
|
from llnl.util.lang import GroupedExceptionHandler
|
||||||
|
|
||||||
import spack.binary_distribution
|
import spack.binary_distribution
|
||||||
import spack.config
|
import spack.config
|
||||||
@ -417,11 +418,10 @@ def _make_bootstrapper(conf):
|
|||||||
return _bootstrap_methods[btype](conf)
|
return _bootstrap_methods[btype](conf)
|
||||||
|
|
||||||
|
|
||||||
def _source_is_trusted(conf):
|
def _validate_source_is_trusted(conf):
|
||||||
trusted, name = spack.config.get('bootstrap:trusted'), conf['name']
|
trusted, name = spack.config.get('bootstrap:trusted'), conf['name']
|
||||||
if name not in trusted:
|
if name not in trusted:
|
||||||
return False
|
raise ValueError('source is not trusted')
|
||||||
return trusted[name]
|
|
||||||
|
|
||||||
|
|
||||||
def spec_for_current_python():
|
def spec_for_current_python():
|
||||||
@ -488,34 +488,25 @@ def ensure_module_importable_or_raise(module, abstract_spec=None):
|
|||||||
abstract_spec = abstract_spec or module
|
abstract_spec = abstract_spec or module
|
||||||
source_configs = spack.config.get('bootstrap:sources', [])
|
source_configs = spack.config.get('bootstrap:sources', [])
|
||||||
|
|
||||||
errors = {}
|
h = GroupedExceptionHandler()
|
||||||
|
|
||||||
for current_config in source_configs:
|
for current_config in source_configs:
|
||||||
if not _source_is_trusted(current_config):
|
with h.forward(current_config['name']):
|
||||||
msg = ('[BOOTSTRAP MODULE {0}] Skipping source "{1}" since it is '
|
_validate_source_is_trusted(current_config)
|
||||||
'not trusted').format(module, current_config['name'])
|
|
||||||
tty.debug(msg)
|
|
||||||
continue
|
|
||||||
|
|
||||||
b = _make_bootstrapper(current_config)
|
b = _make_bootstrapper(current_config)
|
||||||
try:
|
|
||||||
if b.try_import(module, abstract_spec):
|
if b.try_import(module, abstract_spec):
|
||||||
return
|
return
|
||||||
except Exception as e:
|
|
||||||
msg = '[BOOTSTRAP MODULE {0}] Unexpected error "{1}"'
|
|
||||||
tty.debug(msg.format(module, str(e)))
|
|
||||||
errors[current_config['name']] = e
|
|
||||||
|
|
||||||
# We couldn't import in any way, so raise an import error
|
assert h, 'expected at least one exception to have been raised at this point: while bootstrapping {0}'.format(module) # noqa: E501
|
||||||
msg = 'cannot bootstrap the "{0}" Python module'.format(module)
|
msg = 'cannot bootstrap the "{0}" Python module '.format(module)
|
||||||
if abstract_spec:
|
if abstract_spec:
|
||||||
msg += ' from spec "{0}"'.format(abstract_spec)
|
msg += 'from spec "{0}" '.format(abstract_spec)
|
||||||
msg += ' due to the following failures:\n'
|
if tty.is_debug():
|
||||||
for method in errors:
|
msg += h.grouped_message(with_tracebacks=True)
|
||||||
err = errors[method]
|
else:
|
||||||
msg += " '{0}' raised {1}: {2}\n".format(
|
msg += h.grouped_message(with_tracebacks=False)
|
||||||
method, err.__class__.__name__, str(err))
|
msg += '\nRun `spack --debug ...` for more detailed errors'
|
||||||
msg += ' Please run `spack -d spec zlib` for more verbose error messages'
|
|
||||||
raise ImportError(msg)
|
raise ImportError(msg)
|
||||||
|
|
||||||
|
|
||||||
@ -539,15 +530,14 @@ def ensure_executables_in_path_or_raise(executables, abstract_spec):
|
|||||||
|
|
||||||
executables_str = ', '.join(executables)
|
executables_str = ', '.join(executables)
|
||||||
source_configs = spack.config.get('bootstrap:sources', [])
|
source_configs = spack.config.get('bootstrap:sources', [])
|
||||||
for current_config in source_configs:
|
|
||||||
if not _source_is_trusted(current_config):
|
|
||||||
msg = ('[BOOTSTRAP EXECUTABLES {0}] Skipping source "{1}" since it is '
|
|
||||||
'not trusted').format(executables_str, current_config['name'])
|
|
||||||
tty.debug(msg)
|
|
||||||
continue
|
|
||||||
|
|
||||||
b = _make_bootstrapper(current_config)
|
h = GroupedExceptionHandler()
|
||||||
try:
|
|
||||||
|
for current_config in source_configs:
|
||||||
|
with h.forward(current_config['name']):
|
||||||
|
_validate_source_is_trusted(current_config)
|
||||||
|
|
||||||
|
b = _make_bootstrapper(current_config)
|
||||||
if b.try_search_path(executables, abstract_spec):
|
if b.try_search_path(executables, abstract_spec):
|
||||||
# Additional environment variables needed
|
# Additional environment variables needed
|
||||||
concrete_spec, cmd = b.last_search['spec'], b.last_search['command']
|
concrete_spec, cmd = b.last_search['spec'], b.last_search['command']
|
||||||
@ -562,14 +552,16 @@ def ensure_executables_in_path_or_raise(executables, abstract_spec):
|
|||||||
)
|
)
|
||||||
cmd.add_default_envmod(env_mods)
|
cmd.add_default_envmod(env_mods)
|
||||||
return cmd
|
return cmd
|
||||||
except Exception as e:
|
|
||||||
msg = '[BOOTSTRAP EXECUTABLES {0}] Unexpected error "{1}"'
|
|
||||||
tty.debug(msg.format(executables_str, str(e)))
|
|
||||||
|
|
||||||
# We couldn't import in any way, so raise an import error
|
assert h, 'expected at least one exception to have been raised at this point: while bootstrapping {0}'.format(executables_str) # noqa: E501
|
||||||
msg = 'cannot bootstrap any of the {0} executables'.format(executables_str)
|
msg = 'cannot bootstrap any of the {0} executables '.format(executables_str)
|
||||||
if abstract_spec:
|
if abstract_spec:
|
||||||
msg += ' from spec "{0}"'.format(abstract_spec)
|
msg += 'from spec "{0}" '.format(abstract_spec)
|
||||||
|
if tty.is_debug():
|
||||||
|
msg += h.grouped_message(with_tracebacks=True)
|
||||||
|
else:
|
||||||
|
msg += h.grouped_message(with_tracebacks=False)
|
||||||
|
msg += '\nRun `spack --debug ...` for more detailed errors'
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +62,22 @@ def test_raising_exception_if_bootstrap_disabled(mutable_config):
|
|||||||
spack.bootstrap.store_path()
|
spack.bootstrap.store_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_raising_exception_module_importable():
|
||||||
|
with pytest.raises(
|
||||||
|
ImportError,
|
||||||
|
match='cannot bootstrap the "asdf" Python module',
|
||||||
|
):
|
||||||
|
spack.bootstrap.ensure_module_importable_or_raise("asdf")
|
||||||
|
|
||||||
|
|
||||||
|
def test_raising_exception_executables_in_path():
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError,
|
||||||
|
match="cannot bootstrap any of the asdf, fdsa executables",
|
||||||
|
):
|
||||||
|
spack.bootstrap.ensure_executables_in_path_or_raise(["asdf", "fdsa"], "python")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.regression('25603')
|
@pytest.mark.regression('25603')
|
||||||
def test_bootstrap_deactivates_environments(active_mock_environment):
|
def test_bootstrap_deactivates_environments(active_mock_environment):
|
||||||
assert spack.environment.active_environment() == active_mock_environment
|
assert spack.environment.active_environment() == active_mock_environment
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -270,3 +271,37 @@ def f(*args, **kwargs):
|
|||||||
def test_dedupe():
|
def test_dedupe():
|
||||||
assert [x for x in dedupe([1, 2, 1, 3, 2])] == [1, 2, 3]
|
assert [x for x in dedupe([1, 2, 1, 3, 2])] == [1, 2, 3]
|
||||||
assert [x for x in dedupe([1, -2, 1, 3, 2], key=abs)] == [1, -2, 3]
|
assert [x for x in dedupe([1, -2, 1, 3, 2], key=abs)] == [1, -2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_grouped_exception():
|
||||||
|
h = llnl.util.lang.GroupedExceptionHandler()
|
||||||
|
|
||||||
|
def inner():
|
||||||
|
raise ValueError('wow!')
|
||||||
|
|
||||||
|
with h.forward('inner method'):
|
||||||
|
inner()
|
||||||
|
|
||||||
|
with h.forward('top-level'):
|
||||||
|
raise TypeError('ok')
|
||||||
|
|
||||||
|
assert h.grouped_message(with_tracebacks=False) == dedent("""\
|
||||||
|
due to the following failures:
|
||||||
|
inner method raised ValueError: wow!
|
||||||
|
top-level raised TypeError: ok""")
|
||||||
|
|
||||||
|
assert h.grouped_message(with_tracebacks=True) == dedent("""\
|
||||||
|
due to the following failures:
|
||||||
|
inner method raised ValueError: wow!
|
||||||
|
File "{0}", \
|
||||||
|
line 283, in test_grouped_exception
|
||||||
|
inner()
|
||||||
|
File "{0}", \
|
||||||
|
line 280, in inner
|
||||||
|
raise ValueError('wow!')
|
||||||
|
|
||||||
|
top-level raised TypeError: ok
|
||||||
|
File "{0}", \
|
||||||
|
line 286, in test_grouped_exception
|
||||||
|
raise TypeError('ok')
|
||||||
|
""").format(__file__)
|
||||||
|
Loading…
Reference in New Issue
Block a user