Skip to content

Commit 6f8d0b3

Browse files
committed
Address review + limit each builder to 70 displayed "build dots"
1 parent 564d62b commit 6f8d0b3

File tree

2 files changed

+54
-17
lines changed

2 files changed

+54
-17
lines changed

master/custom/release_dashboard.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434

3535
def _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

5052
class 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")

master/custom/templates/releasedashboard.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ <h5>
196196
</div>
197197
{% endif %}
198198
<div class="build-dots">
199-
{% for build in builder.builds %}
199+
{% for build in builder.builds[:70] %}
200200
{{ build_dot(build) }}
201201
{% endfor %}
202202
</div>

0 commit comments

Comments
 (0)