3333
3434
3535def _gimme_error (func ):
36- """Decorator to turn AttributeError into a different Exception
36+ """Debug decorator to turn AttributeError into a different Exception
3737
3838 jinja2 tends to swallow AttributeError or report it in some place it
3939 didn't happen. When that's a problem, use this decorator to get
@@ -43,9 +43,11 @@ def decorated(*args, **kwargs):
4343 try :
4444 return func (* args , ** kwargs )
4545 except AttributeError as e :
46- raise Exception (f'your error: { e !r} ' )
46+ raise _WrappedAttributeError (f'your error: { e !r} ' )
4747 return decorated
4848
49+ class _WrappedAttributeError (Exception ): pass
50+
4951
5052class DashboardObject :
5153 """Base wrapper for a dashboard object.
@@ -307,13 +309,10 @@ def problems(self):
307309
308310 @cached_property
309311 def featured_problem (self ):
310- try :
311312 try :
312313 return self .problems [0 ]
313314 except IndexError :
314315 return NoProblem ()
315- except AttributeError :
316- raise SystemError
317316
318317 def get_grouped_problems (self ):
319318 def key (problem ):
@@ -411,18 +410,35 @@ def css_color_class(self):
411410
412411 @cached_property
413412 def junit_results (self ):
414- filepath = (
415- self ._root ._app .test_result_dir
416- / self .builder .branch .tag
417- / self .builder ["name" ]
418- / f'build_{ self ["number" ]} .xml'
419- )
413+ if not self ._root ._app .test_result_dir :
414+ return None
415+
420416 try :
421- file = filepath .open ()
422- except OSError :
417+ filepath = (
418+ self ._root ._app .test_result_dir
419+ / self .builder .branch .tag
420+ / self .builder ["name" ]
421+ / f'build_{ self ["number" ]} .xml'
422+ ).resolve ()
423+
424+ # Ensure path doesn't escape test_result_dir
425+ if not filepath .is_relative_to (self ._root ._app .test_result_dir ):
426+ return None
427+
428+ if not filepath .is_file ():
429+ return None
430+
431+ with filepath .open () as file :
432+ etree = ElementTree .parse (file )
433+
434+ # We don't have a logger set up, this returns None on common failures
435+ # (meaning failures won't show on the dashboard).
436+ # TODO: set up monitoring and log failures (in the whole method).
437+ except OSError as e :
438+ return None
439+ except ElementTree .ParseError as e :
423440 return None
424- with file :
425- etree = ElementTree .parse (file )
441+
426442 result = JunitResult (self , {})
427443 for element in etree .iterfind ('.//error/..' ):
428444 result .add (element )
@@ -449,6 +465,22 @@ def __init__(self, *args):
449465 self .error_types = set ()
450466
451467 def add (self , element ):
468+ """Add errors from a XML element.
469+
470+ JunitResult are arranged in a tree, grouped by test modules, classes
471+ and methods (i.e. dot-separated parts of the test name).
472+
473+ JunitError instances are added to the lowest level of the tree.
474+ They're deduplicated, because we re-run failing tests and often
475+ get two copies of the same error (with the same traceback).
476+
477+ Exception type names are added to *all* levels of the tree:
478+ if the details of a test module/class/methods aren't expanded,
479+ the dashboard shows exception types from all the hidden failures.
480+ """
481+ # Gather all the errors (as dicts), and their exception types
482+ # (as strings), from *element*.
483+ # Usually there's only one error per element.
452484 errors = []
453485 error_types = set ()
454486 for error_elem in element .iterfind ('error' ):
@@ -458,13 +490,18 @@ def add(self, element):
458490 })
459491 errors .append (new_error )
460492 error_types .add (new_error ["type" ])
493+
494+ # Find/add the leaf JunitResult, updating result.error_types for each
495+ # Result along the way
461496 result = self
462497 name_parts = element .attrib .get ('name' , '??' ).split ('.' )
463498 if name_parts [0 ] == 'test' :
464499 name_parts .pop (0 )
465500 for part in name_parts :
466501 result .error_types .update (error_types )
467502 result = result .contents .setdefault (part , JunitResult (self , {}))
503+
504+ # Add error details to the leaf
468505 result .error_types .update (error_types )
469506 for error in errors :
470507 if error not in result .errors :
@@ -665,7 +702,7 @@ def __init__(self, test_result_dir=None):
665702 self .flask_app .jinja_env .add_extension ('jinja2.ext.loopcontrols' )
666703 self .flask_app .jinja_env .undefined = jinja2 .StrictUndefined
667704
668- self .test_result_dir = Path (test_result_dir )
705+ self .test_result_dir = Path (test_result_dir ). resolve ()
669706
670707 @self .flask_app .route ('/' )
671708 @self .flask_app .route ("/index.html" )
0 commit comments