Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI Option to Show Full Tracebacks for Error #760

Draft
wants to merge 14 commits into
base: develop
Choose a base branch
from
4 changes: 4 additions & 0 deletions lib/pavilion/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def get_parser():
'--profile-count', default=PROFILE_COUNT_DEFAULT, action='store', type=int,
help="Number of rows in the profile table.")

parser.add_argument(
'--show-tracebacks', dest='show_tracebacks', action='store_true',
help="Display full traceback when printing error messages.")

_PAV_PARSER = parser
_PAV_SUB_PARSER = parser.add_subparsers(dest='command_name')

Expand Down
3 changes: 2 additions & 1 deletion lib/pavilion/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pavilion import groups
from pavilion import lockfile
from pavilion import utils
from pavilion import config
from pavilion.builder import TestBuilder
from pavilion.test_run import test_run_attr_transform, TestAttributes

Expand Down Expand Up @@ -74,7 +75,7 @@ def delete_unused_builds(pav_cfg, builds_dir: Path, tests_dir: Path, verbose: bo
return count, msgs


def clean_groups(pav_cfg) -> Tuple[int, List[str]]:
def clean_groups(pav_cfg: config.PavConfig) -> Tuple[int, List[str]]:
"""Remove members that no longer exist from groups, and delete empty groups.
Returns the number of groups deleted and a list of error messages."""

Expand Down
8 changes: 5 additions & 3 deletions lib/pavilion/cmd_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import sys
import time
from pathlib import Path
from typing import List, TextIO, Union, Iterator
from typing import List, TextIO, Union, Optional, Iterator
from collections import defaultdict

from pavilion import config
Expand Down Expand Up @@ -321,7 +321,8 @@ def get_collection_path(pav_cfg, collection) -> Union[Path, None]:
return None


def test_list_to_paths(pav_cfg, req_tests, errfile=None) -> List[Path]:
def test_list_to_paths(pav_cfg: config.PavConfig, req_tests: List[str],
errfile: Optional[Path] = None) -> List[Path]:
"""Given a list of raw test id's and series id's, return a list of paths
to those tests.
The keyword 'last' may also be given to get the last series run by
Expand All @@ -338,7 +339,6 @@ def test_list_to_paths(pav_cfg, req_tests, errfile=None) -> List[Path]:

test_paths = []
for raw_id in req_tests:

if raw_id == 'last':
raw_id = series.load_user_series_id(pav_cfg, errfile)
if raw_id is None:
Expand All @@ -359,10 +359,12 @@ def test_list_to_paths(pav_cfg, req_tests, errfile=None) -> List[Path]:

test_path = test_wd/TestRun.RUN_DIR/str(_id)
test_paths.append(test_path)

if not test_path.exists():
output.fprint(errfile,
"Test run with id '{}' could not be found.".format(raw_id),
color=output.YELLOW)

elif raw_id[0] == 's' and utils.is_int(raw_id[1:]):
# A series.
try:
Expand Down
5 changes: 4 additions & 1 deletion lib/pavilion/commands/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import errno
import fnmatch
from typing import Optional

from pavilion import groups
from pavilion import config
Expand Down Expand Up @@ -122,21 +123,23 @@ def run(self, pav_cfg, args):

return self._run_sub_command(pav_cfg, args)

def _get_group(self, pav_cfg, group_name: str) -> TestGroup:
def _get_group(self, pav_cfg: config.PavConfig, group_name: str) -> Optional[TestGroup]:
"""Get the requested group, and print a standard error message on failure."""
hwikle-lanl marked this conversation as resolved.
Show resolved Hide resolved

try:
group = TestGroup(pav_cfg, group_name)
except TestGroupError as err:
fprint(self.errfile, "Error loading group '{}'", color=output.RED)
fprint(self.errfile, err.pformat())

return None

if not group.exists():
fprint(self.errfile,
"Group '{}' does not exist.\n Looked here:"
.format(group_name), color=output.RED)
fprint(self.errfile, " " + group.path.as_posix())

return None

return group
Expand Down
2 changes: 1 addition & 1 deletion lib/pavilion/commands/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def _setup_arguments(self, parser):
state_p.add_argument('series', default='last', nargs='?',
help="The series to print status history for.")

def _find_series(self, pav_cfg, series_name):
def _find_series(self, pav_cfg: config.PavConfig, series_name: int):
"""Grab the series based on the series name, if one was given."""

if series_name == 'last':
Expand Down
30 changes: 28 additions & 2 deletions lib/pavilion/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@
import pprint
import textwrap
import shutil
import traceback

from traceback import format_exception
from typing import List

import lark

import yc_yaml

from pavilion.micro import flatten


class PavilionError(RuntimeError):
"""Base class for all Pavilion errors."""

SPLIT_RE = re.compile(': *\n? *')
TAB_LEVEL = ' '

# Set traceback behavior for all instances
show_tracebacks = False

def __init__(self, msg, prior_error=None, data=None):
"""These take a new message and whatever prior error caused the problem.

Expand All @@ -30,6 +39,7 @@ def __init__(self, msg, prior_error=None, data=None):
self.data = data
super().__init__(msg)


@property
def msg(self):
"""Just return msg. This exists to be overridden in order to allow for
Expand All @@ -46,12 +56,28 @@ def __str__(self):
else:
return self.msg

@staticmethod
def _wrap_lines(lines: List[str], width: int) -> List[str]:
"""Given a list of lines, produce a new list of lines wrapped to the specified width."""

lines = map(lambda x: textwrap.wrap(x, width=width), lines)

return list(flatten(lines))


def pformat(self) -> str:
"""Specially format the exception for printing."""
"""Specially format the exception for printing. If traceback is True, return the full
traceback associated with the error. Otherwise, return a summary of the error."""

width = shutil.get_terminal_size((80, 80)).columns

if PavilionError.show_tracebacks:
lines = format_exception(PavilionError, self, self.__traceback__)

return "".join(lines)

lines = []
next_exc = self.prior_error
width = shutil.get_terminal_size((80, 80)).columns
tab_level = 0
for line in str(self.msg).split('\n'):
lines.extend(textwrap.wrap(line, width=width))
Expand Down
26 changes: 16 additions & 10 deletions lib/pavilion/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import pavilion.commands
import pavilion.errors

from pavilion.errors import PavilionError
from . import arguments
from . import commands
from . import config
Expand Down Expand Up @@ -41,8 +43,7 @@ def main():
# Pavilion is compatible with python >= 3.4
if (sys.version_info[0] != SUPPORTED_MAJOR_VERSION
or sys.version_info[1] < MIN_SUPPORTED_MINOR_VERSION):
output.fprint(sys.stderr, "Pavilion requires python 3.6 or higher.", color=output.RED)
sys.exit(-1)
raise PavilionError("Pavilion requires python 3.6 or higher.")

# This has to be done before we initialize plugins
parser = arguments.get_parser()
Expand All @@ -51,8 +52,7 @@ def main():
try:
pav_cfg = config.find_pavilion_config()
except Exception as err:
output.fprint(sys.stderr, "Error getting config, exiting.", err, color=output.RED)
sys.exit(-1)
raise PavilionError("Error getting config, exiting.") from err

# Setup all the loggers for Pavilion
log_output = log_setup.setup_loggers(pav_cfg)
Expand All @@ -61,8 +61,7 @@ def main():
try:
plugins.initialize_plugins(pav_cfg)
except pavilion.errors.PluginError as err:
output.fprint(sys.stderr, "Error initializing plugins.", err, color=output.RED)
sys.exit(-1)
raise PavilionError("Error initializing plugins.") from err

# Partially parse the arguments. All we really care about is the subcommand.
partial_args, _ = parser.parse_known_args()
Expand Down Expand Up @@ -178,7 +177,14 @@ def profile_main():


if __name__ == '__main__':
if '--profile' in sys.argv:
profile_main()
else:
main()
if '--show-tracebacks' in sys.argv:
PavilionError.show_tracebacks = True

try:
if '--profile' in sys.argv:
profile_main()
else:
main()
except PavilionError as err:
err.pformat()
exit(-1)
3 changes: 2 additions & 1 deletion lib/pavilion/series/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,8 @@ def run(self, build_only: bool = False, rebuild: bool = False,
# Completion will be set when looked for.


def _run_set(self, test_set: TestSet, build_only: bool, rebuild: bool, local_builds_only: bool):
def _run_set(self, test_set: TestSet, build_only: bool, rebuild: bool,
local_builds_only: bool):
"""Run all requested tests in the given test set."""

# Track which builds we've already marked as deprecated, when doing rebuilds.
Expand Down
86 changes: 54 additions & 32 deletions test/tests/errors_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,70 @@


class ErrorTests(unittest.PavTestCase):
"""Test functionaility of Pavilion specific errors."""
"""Test functionaility of Pavilion specific errors."""

def test_error_pickling(self):
"""Check that all of the Pavilon errors pickle and unpickle correctly."""
def test_error_pickling(self):
"""Check that all of the Pavilon errors pickle and unpickle correctly."""


prior_error = ValueError("hiya")
prior_error = ValueError("hiya")

base_args = (["foo"], )
base_kwargs = {'prior_error':prior_error, 'data': {"foo": "bar"}}
base_args = (["foo"], )
base_kwargs = {'prior_error':prior_error, 'data': {"foo": "bar"}}

spec_args = {
'VariableError': (('hello',),
{'var_set': 'var', 'var': 'foo',
'index': 3, 'sub_var': 'ok', 'prior_error': prior_error}),
'DeferredError': (('hello',),
{'var_set': 'var', 'var': 'foo',
'index': 3, 'sub_var': 'ok', 'prior_error': prior_error}),
'ParserValueError': ((Token('oh_no', 'terrible_things'), 'hello'), {})
}
spec_args = {
'VariableError': (('hello',),
{'var_set': 'var', 'var': 'foo',
'index': 3, 'sub_var': 'ok', 'prior_error': prior_error}),
'DeferredError': (('hello',),
{'var_set': 'var', 'var': 'foo',
'index': 3, 'sub_var': 'ok', 'prior_error': prior_error}),
'ParserValueError': ((Token('oh_no', 'terrible_things'), 'hello'), {})
}

base_attrs = dir(errors.PavilionError("foo"))
base_attrs = dir(errors.PavilionError("foo"))

exc_classes = []
for name in dir(errors):
obj = getattr(errors, name)
if (type(obj) == type(errors.PavilionError)
and issubclass(obj, errors.PavilionError)):
exc_classes.append(obj)
exc_classes = []
for name in dir(errors):
obj = getattr(errors, name)
if (type(obj) == type(errors.PavilionError)
and issubclass(obj, errors.PavilionError)):
exc_classes.append(obj)

for exc_class in exc_classes:
exc_name = exc_class.__name__
for exc_class in exc_classes:
exc_name = exc_class.__name__

args, kwargs = spec_args.get(exc_name, (base_args, base_kwargs))
args, kwargs = spec_args.get(exc_name, (base_args, base_kwargs))

inst = exc_class(*args, **kwargs)
inst = exc_class(*args, **kwargs)

p_str = pickle.dumps(inst)
p_str = pickle.dumps(inst)

try:
new_inst = pickle.loads(p_str)
except TypeError:
self.fail("Failed to reconstitute exception '{}'".format(exc_name))
try:
new_inst = pickle.loads(p_str)
except TypeError:
self.fail("Failed to reconstitute exception '{}'".format(exc_name))

self.assertEqual(inst, new_inst)
self.assertEqual(inst, new_inst)

def test_pformat(self):
"""Test that pformat formats errors as expected, including when Pavilion
is set to show full tracebacks for errors."""

try:
try:
raise RuntimeError("Raised a RuntimeError as a test")
except RuntimeError as err:
raise errors.PavilionError("Match this") from err
except errors.PavilionError as err:
self.assertEqual(err.pformat(), "Match this")

errors.PavilionError.show_tracebacks = True

try:
try:
raise RuntimeError("Raised a RuntimeError as a test")
except RuntimeError as err:
raise errors.PavilionError("Match this") from err
except errors.PavilionError as err:
self.assertTrue(err.pformat().startswith("Traceback"))
Loading