Skip to content

Commit bc31997

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1552 from vkarak/feat/failure-info-verbosity
[feat] Control verbosity of check failure info
2 parents 1879eab + 43c30eb commit bc31997

File tree

7 files changed

+155
-81
lines changed

7 files changed

+155
-81
lines changed

reframe/core/decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def _instantiate_all():
4848
try:
4949
ret.append(_instantiate(cls, args))
5050
except Exception:
51-
frame = user_frame(sys.exc_info()[2])
51+
frame = user_frame(*sys.exc_info())
5252
msg = "skipping test due to errors: %s: " % cls.__name__
5353
msg += "use `-v' for more information\n"
5454
msg += " FILE: %s:%s" % (frame.filename, frame.lineno)

reframe/core/exceptions.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,17 @@ class DependencyError(ReframeError):
270270
'''Raised when a dependency problem is encountered.'''
271271

272272

273-
def user_frame(tb):
273+
def user_frame(exc_type, exc_value, tb):
274+
'''Return a user frame from the exception's traceback.
275+
276+
As user frame is considered the first frame that is outside from
277+
:mod:`reframe` module.
278+
279+
:returns: A frame object or :class:`None` if no user frame was found.
280+
281+
:meta private:
282+
283+
'''
274284
if not inspect.istraceback(tb):
275285
raise ValueError('could not retrieve frame: argument not a traceback')
276286

@@ -282,40 +292,52 @@ def user_frame(tb):
282292
return None
283293

284294

285-
def format_exception(exc_type, exc_value, tb):
286-
def format_user_frame(frame):
287-
relpath = os.path.relpath(frame.filename)
288-
return '%s:%s: %s\n%s' % (relpath, frame.lineno,
289-
exc_value, ''.join(frame.code_context))
290-
291-
if exc_type is None:
292-
return ''
295+
def is_severe(exc_type, exc_value, tb):
296+
'''Check if exception is a severe one.'''
297+
soft_errors = (ReframeError,
298+
ConnectionError,
299+
FileExistsError,
300+
FileNotFoundError,
301+
IsADirectoryError,
302+
KeyboardInterrupt,
303+
NotADirectoryError,
304+
PermissionError,
305+
TimeoutError)
306+
if isinstance(exc_value, soft_errors):
307+
return False
293308

294-
if isinstance(exc_value, AbortTaskError):
295-
return 'aborted due to %s' % type(exc_value.__cause__).__name__
309+
# Treat specially type and value errors
310+
type_error = isinstance(exc_value, TypeError)
311+
value_error = isinstance(exc_value, ValueError)
312+
frame = user_frame(exc_type, exc_value, tb)
313+
if (type_error or value_error) and frame is not None:
314+
return False
296315

297-
if isinstance(exc_value, ReframeError):
298-
return '%s: %s' % (utility.decamelize(exc_type.__name__, ' '),
299-
exc_value)
316+
return True
300317

301-
if isinstance(exc_value, ReframeFatalError):
302-
exc_str = '%s: %s' % (utility.decamelize(exc_type.__name__, ' '),
303-
exc_value)
304-
tb_str = ''.join(traceback.format_exception(exc_type, exc_value, tb))
305-
return '%s\n%s' % (exc_str, tb_str)
306318

307-
if isinstance(exc_value, KeyboardInterrupt):
308-
return 'cancelled by user'
319+
def what(exc_type, exc_value, tb):
320+
'''A short description of the error.'''
309321

310-
if isinstance(exc_value, OSError):
311-
return 'OS error: %s' % exc_value
322+
if exc_type is None:
323+
return ''
312324

313-
frame = user_frame(tb)
314-
if isinstance(exc_value, TypeError) and frame is not None:
315-
return 'type error: ' + format_user_frame(frame)
325+
reason = utility.decamelize(exc_type.__name__, ' ')
316326

317-
if isinstance(exc_value, ValueError) and frame is not None:
318-
return 'value error: ' + format_user_frame(frame)
327+
# We need frame information for user type and value errors
328+
frame = user_frame(exc_type, exc_value, tb)
329+
user_type_error = isinstance(exc_value, TypeError) and frame
330+
user_value_error = isinstance(exc_value, ValueError) and frame
331+
if isinstance(exc_value, KeyboardInterrupt):
332+
reason = 'cancelled by user'
333+
elif isinstance(exc_value, AbortTaskError):
334+
reason = f'aborted due to {type(exc_value.__cause__).__name__}'
335+
elif user_type_error or user_value_error:
336+
relpath = os.path.relpath(frame.filename)
337+
source = ''.join(frame.code_context)
338+
reason += f': {relpath}:{frame.lineno}: {exc_value}\n{source}'
339+
else:
340+
if str(exc_value):
341+
reason += f': {exc_value}'
319342

320-
exc_str = ''.join(traceback.format_exception(exc_type, exc_value, tb))
321-
return 'unexpected error: %s\n%s' % (exc_value, exc_str)
343+
return reason

reframe/frontend/cli.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,15 @@
1515
import reframe
1616
import reframe.core.config as config
1717
import reframe.core.environments as env
18+
import reframe.core.exceptions as errors
1819
import reframe.core.logging as logging
1920
import reframe.core.runtime as runtime
21+
import reframe.core.warnings as warnings
2022
import reframe.frontend.argparse as argparse
2123
import reframe.frontend.check_filters as filters
2224
import reframe.frontend.dependency as dependency
2325
import reframe.utility.jsonext as jsonext
2426
import reframe.utility.osext as osext
25-
from reframe.core.exceptions import (
26-
EnvironError, ConfigError, ReframeError,
27-
ReframeFatalError, format_exception
28-
)
29-
from reframe.core.warnings import ReframeDeprecationWarning
3027
from reframe.frontend.executors import Runner, generate_testcases
3128
from reframe.frontend.executors.policies import (SerialExecutionPolicy,
3229
AsynchronousExecutionPolicy)
@@ -453,7 +450,7 @@ def main():
453450
try:
454451
try:
455452
site_config = config.load_config(options.config_file)
456-
except ReframeDeprecationWarning as e:
453+
except warnings.ReframeDeprecationWarning as e:
457454
printer.warning(e)
458455
converted = config.convert_old_config(options.config_file)
459456
printer.warning(
@@ -483,7 +480,7 @@ def main():
483480
options.update_config(site_config)
484481

485482
logging.configure_logging(site_config)
486-
except (OSError, ConfigError) as e:
483+
except (OSError, errors.ConfigError) as e:
487484
printer.error(f'failed to load configuration: {e}')
488485
sys.exit(1)
489486

@@ -492,7 +489,7 @@ def main():
492489
printer.inc_verbosity(site_config.get('general/0/verbose'))
493490
try:
494491
runtime.init_runtime(site_config)
495-
except ConfigError as e:
492+
except errors.ConfigError as e:
496493
printer.error(f'failed to initialize runtime: {e}')
497494
sys.exit(1)
498495

@@ -507,7 +504,7 @@ def main():
507504
for m in site_config.get('general/0/module_mappings'):
508505
rt.modules_system.load_mapping(m)
509506

510-
except (ConfigError, OSError) as e:
507+
except (errors.ConfigError, OSError) as e:
511508
printer.error('could not load module mappings: %s' % e)
512509
sys.exit(1)
513510

@@ -580,7 +577,7 @@ def print_infoline(param, value):
580577
try:
581578
checks_found = loader.load_all()
582579
except OSError as e:
583-
raise ReframeError from e
580+
raise errors.ReframeError from e
584581

585582
# Filter checks by name
586583
checks_matched = checks_found
@@ -651,7 +648,7 @@ def print_infoline(param, value):
651648
# Load the environment for the current system
652649
try:
653650
runtime.loadenv(rt.system.preload_environ)
654-
except EnvironError as e:
651+
except errors.EnvironError as e:
655652
printer.error("failed to load current system's environment; "
656653
"please check your configuration")
657654
printer.debug(str(e))
@@ -660,7 +657,7 @@ def print_infoline(param, value):
660657
for m in site_config.get('general/0/user_modules'):
661658
try:
662659
rt.modules_system.load_module(m, force=True)
663-
except EnvironError as e:
660+
except errors.EnvironError as e:
664661
printer.warning("could not load module '%s' correctly: "
665662
"Skipping..." % m)
666663
printer.debug(str(e))
@@ -695,7 +692,9 @@ def print_infoline(param, value):
695692
errmsg = "invalid option for --flex-alloc-nodes: '{0}'"
696693
sched_flex_alloc_nodes = int(options.flex_alloc_nodes)
697694
if sched_flex_alloc_nodes <= 0:
698-
raise ConfigError(errmsg.format(options.flex_alloc_nodes))
695+
raise errors.ConfigError(
696+
errmsg.format(options.flex_alloc_nodes)
697+
)
699698
except ValueError:
700699
sched_flex_alloc_nodes = options.flex_alloc_nodes
701700

@@ -713,8 +712,9 @@ def print_infoline(param, value):
713712
try:
714713
max_retries = int(options.max_retries)
715714
except ValueError:
716-
raise ConfigError('--max-retries is not a valid integer: %s' %
717-
max_retries) from None
715+
raise errors.ConfigError(
716+
f'--max-retries is not a valid integer: {max_retries}'
717+
) from None
718718
runner = Runner(exec_policy, printer, max_retries)
719719
try:
720720
time_start = time.time()
@@ -735,10 +735,10 @@ def print_infoline(param, value):
735735

736736
# Print a failure report if we had failures in the last run
737737
if runner.stats.failures():
738-
printer.info(runner.stats.failure_report())
738+
runner.stats.print_failure_report(printer)
739739
success = False
740740
if options.failure_stats:
741-
printer.info(runner.stats.failure_stats())
741+
runner.stats.print_failure_stats(printer)
742742

743743
if options.performance_report:
744744
printer.info(runner.stats.performance_report())
@@ -784,11 +784,18 @@ def print_infoline(param, value):
784784

785785
except KeyboardInterrupt:
786786
sys.exit(1)
787-
except ReframeError as e:
787+
except errors.ReframeError as e:
788788
printer.error(str(e))
789789
sys.exit(1)
790-
except (Exception, ReframeFatalError):
791-
printer.error(format_exception(*sys.exc_info()))
790+
except (Exception, errors.ReframeFatalError):
791+
exc_info = sys.exc_info()
792+
tb = ''.join(traceback.format_exception(*exc_info))
793+
printer.error(errors.what(*exc_info))
794+
if errors.is_severe(*exc_info):
795+
printer.error(tb)
796+
else:
797+
printer.verbose(tb)
798+
792799
sys.exit(1)
793800
finally:
794801
try:
@@ -797,7 +804,7 @@ def print_infoline(param, value):
797804
log_files = logging.save_log_files(rt.output_prefix)
798805

799806
except OSError as e:
800-
printer.error('could not save log file: %s' % e)
807+
printer.error(f'could not save log file: {e}')
801808
sys.exit(1)
802809
finally:
803810
if not log_files:

reframe/frontend/statistics.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
#
44
# SPDX-License-Identifier: BSD-3-Clause
55

6+
import traceback
67
import reframe.core.runtime as rt
7-
from reframe.core.exceptions import format_exception, StatisticsError
8+
import reframe.core.exceptions as errors
89

910

1011
class TestStats:
@@ -28,7 +29,7 @@ def tasks(self, run=-1):
2829
try:
2930
return self._alltasks[run]
3031
except IndexError:
31-
raise StatisticsError('no such run: %s' % run) from None
32+
raise errors.StatisticsError('no such run: %s' % run) from None
3233

3334
def failures(self, run=-1):
3435
return [t for t in self.tasks(run) if t.failed]
@@ -130,7 +131,13 @@ def json(self, force=False):
130131
entry['stagedir'] = check.stagedir
131132
entry['fail_phase'] = t.failed_stage
132133
if t.exc_info is not None:
133-
entry['fail_reason'] = format_exception(*t.exc_info)
134+
entry['fail_reason'] = errors.what(*t.exc_info)
135+
entry['fail_info'] = {
136+
'exc_type': t.exc_info[0],
137+
'exc_value': t.exc_info[1],
138+
'traceback': t.exc_info[2]
139+
}
140+
entry['fail_severe'] = errors.is_severe(*t.exc_info)
134141
else:
135142
entry['result'] = 'success'
136143
entry['outputdir'] = check.outputdir
@@ -161,10 +168,10 @@ def json(self, force=False):
161168

162169
return self._run_data
163170

164-
def failure_report(self):
171+
def print_failure_report(self, printer):
165172
line_width = 78
166-
report = [line_width * '=']
167-
report.append('SUMMARY OF FAILURES')
173+
printer.info(line_width * '=')
174+
printer.info('SUMMARY OF FAILURES')
168175
run_report = self.json()[-1]
169176
last_run = run_report['runid']
170177
for r in run_report['testcases']:
@@ -174,27 +181,32 @@ def failure_report(self):
174181
retry_info = (
175182
f'(for the last of {last_run} retries)' if last_run > 0 else ''
176183
)
177-
report.append(line_width * '-')
178-
report.append(f"FAILURE INFO for {r['name']} {retry_info}")
179-
report.append(f" * Test Description: {r['description']}")
180-
report.append(f" * System partition: {r['system']}")
181-
report.append(f" * Environment: {r['environment']}")
182-
report.append(f" * Stage directory: {r['stagedir']}")
184+
printer.info(line_width * '-')
185+
printer.info(f"FAILURE INFO for {r['name']} {retry_info}")
186+
printer.info(f" * Test Description: {r['description']}")
187+
printer.info(f" * System partition: {r['system']}")
188+
printer.info(f" * Environment: {r['environment']}")
189+
printer.info(f" * Stage directory: {r['stagedir']}")
183190
nodelist = ','.join(r['nodelist']) if r['nodelist'] else None
184-
report.append(f" * Node list: {nodelist}")
191+
printer.info(f" * Node list: {nodelist}")
185192
job_type = 'local' if r['scheduler'] == 'local' else 'batch job'
186193
jobid = r['jobid']
187-
report.append(f" * Job type: {job_type} (id={r['jobid']})")
188-
report.append(f" * Maintainers: {r['maintainers']}")
189-
report.append(f" * Failing phase: {r['fail_phase']}")
190-
report.append(f" * Rerun with '-n {r['name']}"
191-
f" -p {r['environment']} --system {r['system']}'")
192-
report.append(f" * Reason: {r['fail_reason']}")
193-
194-
report.append(line_width * '-')
195-
return '\n'.join(report)
196-
197-
def failure_stats(self):
194+
printer.info(f" * Job type: {job_type} (id={r['jobid']})")
195+
printer.info(f" * Maintainers: {r['maintainers']}")
196+
printer.info(f" * Failing phase: {r['fail_phase']}")
197+
printer.info(f" * Rerun with '-n {r['name']}"
198+
f" -p {r['environment']} --system {r['system']}'")
199+
printer.info(f" * Reason: {r['fail_reason']}")
200+
201+
tb = ''.join(traceback.format_exception(*r['fail_info'].values()))
202+
if r['fail_severe']:
203+
printer.info(tb)
204+
else:
205+
printer.verbose(tb)
206+
207+
printer.info(line_width * '-')
208+
209+
def print_failure_stats(self, printer):
198210
failures = {}
199211
current_run = rt.runtime().current_run
200212
for tf in (t for t in self.tasks(current_run) if t.failed):
@@ -234,9 +246,8 @@ def failure_stats(self):
234246
stats_body.append(row_format.format('', '', str(f)))
235247

236248
if stats_body:
237-
return '\n'.join([stats_start, stats_title, *stats_body,
238-
stats_end])
239-
return ''
249+
for line in (stats_start, stats_title, *stats_body, stats_end):
250+
printer.info(line)
240251

241252
def performance_report(self):
242253
# FIXME: Adapt this function to use the JSON report

reframe/schemas/runreport.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,21 @@
4141
"build_stdout": {"type": ["string", "null"]},
4242
"description": {"type": "string"},
4343
"environment": {"type": ["string", "null"]},
44+
"fail_info": {
45+
"type": ["object", "null"],
46+
"properties": {
47+
"exc_type": {"type": "string"},
48+
"exc_value": {"type": "string"},
49+
"traceback": {
50+
"type": "array",
51+
"items": {"type": "string"}
52+
}
53+
},
54+
"required": ["exc_type", "exc_value", "traceback"]
55+
},
4456
"fail_phase": {"type": ["string", "null"]},
4557
"fail_reason": {"type": ["string", "null"]},
58+
"fail_severe": {"type": "boolean"},
4659
"jobid": {"type": ["number", "null"]},
4760
"job_stderr": {"type": ["string", "null"]},
4861
"job_stdout": {"type": ["string", "null"]},

0 commit comments

Comments
 (0)