Skip to content

Commit 11dc917

Browse files
committed
mx python3-unittest: add -o option (filter only specified tests)
add missing header
1 parent cfeece5 commit 11dc917

File tree

1 file changed

+170
-50
lines changed

1 file changed

+170
-50
lines changed

graalpython/com.oracle.graal.python.test/src/python_unittests.py

Lines changed: 170 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
1+
# Copyright (c) 2018, Oracle and/or its affiliates.
2+
#
3+
# The Universal Permissive License (UPL), Version 1.0
4+
#
5+
# Subject to the condition set forth below, permission is hereby granted to any
6+
# person obtaining a copy of this software, associated documentation and/or data
7+
# (collectively the "Software"), free of charge and under any and all copyright
8+
# rights in the Software, and any and all patent rights owned or freely
9+
# licensable by each licensor hereunder covering either (i) the unmodified
10+
# Software as contributed to or provided by such licensor, or (ii) the Larger
11+
# Works (as defined below), to deal in both
12+
#
13+
# (a) the Software, and
14+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
15+
# one is included with the Software (each a "Larger Work" to which the
16+
# Software is contributed by such licensors),
17+
#
18+
# without restriction, including without limitation the rights to copy, create
19+
# derivative works of, display, perform, and distribute the Software and make,
20+
# use, sell, offer for sale, import, export, have made, and have sold the
21+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
22+
# either these or other terms.
23+
#
24+
# This license is subject to the following condition:
25+
#
26+
# The above copyright notice and either this complete permission notice or at a
27+
# minimum a reference to the UPL must be included in all copies or substantial
28+
# portions of the Software.
29+
#
30+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36+
# SOFTWARE.
37+
138
import csv
239
import os
340
import re
@@ -228,9 +265,22 @@ class Col(object):
228265
]
229266

230267

268+
class Stat(object):
269+
# unittest level aggregates
270+
UT_TOTAL = "ut_total" # all the unittests
271+
UT_RUNS = 'ut_runs' # all unittests which could run
272+
UT_PASS = 'ut_pass' # all unittests which pass
273+
UT_PERCENT_RUNS = "ut_percent_runs" # all unittests which could run even with failures (percent)
274+
UT_PERCENT_PASS = "ut_percent_pass" # all unittests which could run with no failures (percent)
275+
# test level aggregates
276+
TEST_RUNS = "test_runs" # total number of tests that could be loaded and run even with failures
277+
TEST_PASS = "test_pass" # number of tests which ran
278+
TEST_PERCENT_PASS = "test_percent_pass" # percentage of tests which pass from all running tests (all unittests)
279+
280+
231281
def save_as_csv(report_path, unittests, error_messages, stats, current_date):
232282
rows = []
233-
with open(file_name(CSV_RESULTS_NAME, current_date), 'w') as CSV:
283+
with open(report_path, 'w') as CSV:
234284
totals = {
235285
Col.NUM_TESTS: 0,
236286
Col.NUM_FAILS: 0,
@@ -266,16 +316,23 @@ def save_as_csv(report_path, unittests, error_messages, stats, current_date):
266316
else:
267317
total_not_run_at_all += 1
268318

269-
_all_runs = len(unittests)-total_not_run_at_all
270-
_all_total = len(unittests)
271-
_percent_all_runs = float(_all_runs) / float(_all_total) * 100.0
272-
_percent_all_full_passes = float(total_pass_all) / float(_all_total) * 100.0
319+
# unittest stats
320+
totals[Stat.UT_TOTAL] = len(unittests)
321+
totals[Stat.UT_RUNS] = len(unittests) - total_not_run_at_all
322+
totals[Stat.UT_PASS] = total_pass_all
323+
totals[Stat.UT_PERCENT_RUNS] = float(totals[Stat.UT_RUNS]) / float(totals[Stat.UT_TOTAL]) * 100.0
324+
totals[Stat.UT_PERCENT_PASS] = float(totals[Stat.UT_PASS]) / float(totals[Stat.UT_TOTAL]) * 100.0
325+
# test stats
326+
totals[Stat.TEST_RUNS] = totals[Col.NUM_TESTS]
327+
totals[Stat.TEST_PASS] = totals[Col.NUM_PASSES]
328+
totals[Stat.TEST_PERCENT_PASS] = float(totals[Stat.TEST_PASS]) / float(totals[Stat.TEST_RUNS]) * 100.0 \
329+
if totals[Stat.TEST_RUNS] else 0
273330

274-
_test_runs = totals[Col.NUM_PASSES]
275-
_test_total = totals[Col.NUM_TESTS]
276-
_percent_test_runs = float(_test_runs) / float(_test_total) * 100.0 if _test_total else 0
277-
278-
rows.append({
331+
writer = csv.DictWriter(CSV, fieldnames=CSV_HEADER)
332+
writer.writeheader()
333+
for row in rows:
334+
writer.writerow(row)
335+
writer.writerow({
279336
Col.UNITTEST: 'TOTAL',
280337
Col.NUM_TESTS: totals[Col.NUM_TESTS],
281338
Col.NUM_FAILS: totals[Col.NUM_FAILS],
@@ -284,16 +341,13 @@ def save_as_csv(report_path, unittests, error_messages, stats, current_date):
284341
Col.NUM_PASSES: totals[Col.NUM_PASSES],
285342
Col.PYTHON_ERRORS: 'Could run {0}/{1} unittests ({2:.2f}%). Unittests which pass completely: {3:.2f}%. '
286343
'Of the ones which ran, could run: {4}/{5} tests ({6:.2f}%)'.format(
287-
_all_runs, _all_total, _percent_all_runs, _percent_all_full_passes,
288-
_test_runs, _test_total, _percent_test_runs)
344+
totals[Stat.UT_RUNS], totals[Stat.UT_TOTAL],
345+
totals[Stat.UT_PERCENT_RUNS], totals[Stat.UT_PERCENT_PASS],
346+
totals[Stat.TEST_PASS], totals[Stat.TEST_PASS],
347+
totals[Stat.TEST_PERCENT_PASS])
289348
})
290349

291-
writer = csv.DictWriter(CSV, fieldnames=CSV_HEADER)
292-
writer.writeheader()
293-
for row in rows:
294-
writer.writerow(row)
295-
296-
return rows
350+
return rows, totals
297351

298352

299353
HTML_TEMPLATE = '''
@@ -370,35 +424,88 @@ def save_as_csv(report_path, unittests, error_messages, stats, current_date):
370424
'''
371425

372426

373-
def save_as_html(report_name, rows, missing_modules, current_date):
427+
def save_as_html(report_name, rows, totals, missing_modules, current_date):
428+
def grid(*components):
429+
def _fmt(cmp):
430+
if isinstance(cmp, tuple):
431+
return '<div class="col-sm-{}">{}</div>'.format(cmp[1], cmp[0])
432+
return '<div class="col-sm">{}</div>'.format(cmp)
433+
return '''
434+
<div class="container" style="width: 100%;">
435+
<div class="row">
436+
{}
437+
</div>
438+
</div>
439+
'''.format('\n'.join([_fmt(cmp) for cmp in components]))
440+
441+
def progress_bar(value, color='success'):
442+
if 0.0 <= value <= 1.0:
443+
value = 100 * value
444+
return '''
445+
<div class="progress">
446+
<div class="progress-bar progress-bar-{color}" role="progressbar" aria-valuenow="{value}"
447+
aria-valuemin="0" aria-valuemax="100" style="width:{value}%">
448+
{value:.2f}% Complete
449+
</div>
450+
</div>
451+
'''.format(color=color, value=value)
452+
453+
def fluid_div(title, div_content):
454+
return '''
455+
<div class="container-fluid">
456+
<div class="panel panel-default">
457+
<div class="panel-heading clickable">
458+
<h3 class="panel-title"><i class="fa fa-minus-square-o" aria-hidden="true">&nbsp;&nbsp;</i>{title}</h3>
459+
</div>
460+
{content}
461+
</div>
462+
</div>
463+
'''.format(title=title, content=div_content)
464+
465+
def ul(title, items):
466+
return fluid_div(title, '<ul class="list-group">{}</ul>'.format('\n'.join([
467+
'<li class="list-group-item">{}</span></li>'.format(itm) for itm in items
468+
])))
469+
374470
def table(tid, tcols, trows):
375-
thead = '''
471+
_thead = '''
376472
<tr class="text-align: right;">
377473
<th data-orderable="false">&nbsp;</th>
378474
{columns}
379475
</tr>
380476
'''.format(columns='\n'.join(['<th>{}</th>'.format(c) for c in tcols]))
381477

382-
format_val = lambda row, k: '<code>{}</code>'.format(row[k]) if k == Col.PYTHON_ERRORS else row[k]
478+
def format_val(row, k):
479+
value = row[k]
480+
if k == Col.PYTHON_ERRORS:
481+
return '<code class="h6">{}</code>'.format(value)
482+
elif k == Col.UNITTEST:
483+
_class = "text-info"
484+
elif k == Col.NUM_PASSES and value > 0:
485+
_class = "text-success"
486+
elif k in [Col.NUM_ERRORS, Col.NUM_FAILS] and value > 0:
487+
_class = "text-danger"
488+
elif k == Col.NUM_SKIPPED and value > 0:
489+
_class = "text-warning"
490+
elif k == Col.NUM_TESTS:
491+
_class = "text-dark"
492+
else:
493+
_class = "text-danger" if value < 0 else "text-muted"
494+
return '<span class="{} h6"><b>{}</b></span>'.format(_class, value)
383495

384-
tbody = '\n'.join([
496+
_tbody = '\n'.join([
385497
'<tr class="{cls}"><td>{i}</td>{vals}</tr>'.format(
386498
cls='info' if i % 2 == 0 else '', i=i,
387499
vals=' '.join(['<td>{}</td>'.format(format_val(row, k)) for k in tcols]))
388500
for i, row in enumerate(trows)])
389501

390-
return '''
391-
<div class="container-fluid">
392-
<div class="panel panel-default">
393-
<div class="panel-heading clickable">
394-
<h3 class="panel-title"><i class="fa fa-minus-square-o" aria-hidden="true">&nbsp;&nbsp;</i>{title}</h3>
395-
</div>
396-
<table id="{tid}" class="table {tclass}" cellspacing="0" width="100%">
397-
<thead>{thead}</thead><tbody>{tbody}</tbody>
398-
</table>
399-
</div>
400-
</div>
401-
'''.format(title='unittest run statistics', tid=tid, tclass='', thead=thead, tbody=tbody)
502+
_table = '''
503+
<table id="{tid}" class="table {tclass}" cellspacing="0" width="100%">
504+
<thead>{thead}</thead><tbody>{tbody}</tbody>
505+
</table>
506+
'''.format(tid=tid, tclass='', thead=_thead, tbody=_tbody)
507+
508+
return fluid_div('<b>cPython unittests</b> run statistics', _table)
402509

403510
scripts = '''
404511
<script src="https:////cdn.datatables.net/{datatables_version}/js/jquery.dataTables.min.js"></script>
@@ -414,21 +521,25 @@ def table(tid, tcols, trows):
414521
</script>
415522
'''
416523

417-
modules_info = '''
418-
<div class="container-fluid">
419-
<div class="panel panel-default">
420-
<div class="panel-heading clickable">
421-
<h3 class="panel-title"><i class="fa fa-minus-square-o" aria-hidden="true">&nbsp;&nbsp;</i>{title}</h3>
422-
</div>
423-
<ul class="list-group">{content}</ul>
424-
</div>
425-
</div>
426-
'''.format(title='missing modules', content='\n'.join([
427-
'<li class="list-group-item"><b>{}</b>&nbsp;<span class="text-muted">count: {}</span></li>'.format(name, cnt)
524+
missing_modules_info = ul('missing modules', [
525+
'<b>{}</b>&nbsp;<span class="text-muted">count: {}</span>'.format(name, cnt)
428526
for cnt, name in sorted(((cnt, name) for name, cnt in missing_modules.items()), reverse=True)
429-
]))
527+
])
528+
529+
total_stats_info = ul("<b>Summary</b>", [
530+
grid('<b># total</b> unittests: {}'.format(totals[Stat.UT_TOTAL])),
531+
grid((progress_bar(totals[Stat.UT_PERCENT_RUNS], color="info"), 3),
532+
'<b># unittest</b> which run: {}'.format(totals[Stat.UT_RUNS])),
533+
grid((progress_bar(totals[Stat.UT_PERCENT_PASS], color="success"), 3),
534+
'<b># unittest</b> which pass: {}'.format(totals[Stat.UT_PASS])),
535+
grid('<b># tests</b> which run: {}'.format(totals[Stat.TEST_RUNS])),
536+
grid((progress_bar(totals[Stat.TEST_PERCENT_PASS], color="info"), 3),
537+
'<b># tests</b> which pass: {}'.format(totals[Stat.TEST_PASS])),
538+
])
430539

431-
content = modules_info + table('stats', CSV_HEADER, rows)
540+
table_stats = table('stats', CSV_HEADER, rows)
541+
542+
content = ' <br> '.join([total_stats_info, table_stats, missing_modules_info])
432543

433544
report = HTML_TEMPLATE.format(
434545
title='GraalPython Unittests Stats',
@@ -459,6 +570,7 @@ def main(prog, args):
459570
parser.add_argument("-v", "--verbose", help="Verbose output.", action="store_true")
460571
parser.add_argument("-l", "--limit", help="Limit the number of unittests to run.", default=None, type=int)
461572
parser.add_argument("-t", "--tests_path", help="Unittests path.", default=PATH_UNITTESTS)
573+
parser.add_argument("-o", "--only_tests", help="Run only these unittests (comma sep values).", default=None)
462574
parser.add_argument("path", help="Path to store the csv output and logs to.", nargs='?', default=None)
463575

464576
global flags
@@ -473,18 +585,26 @@ def main(prog, args):
473585
else:
474586
log("[INFO] results will not be saved remotely")
475587

476-
unittests = get_unittests(flags.tests_path, limit=flags.limit)
588+
if flags.only_tests:
589+
def _fmt(t):
590+
t = t.strip()
591+
return os.path.join(flags.tests_path, t if t.endswith(".py") else t + ".py")
592+
only_tests = set([_fmt(test) for test in flags.only_tests.split(",")])
593+
unittests = [t for t in get_unittests(flags.tests_path) if t in only_tests]
594+
else:
595+
unittests = get_unittests(flags.tests_path, limit=flags.limit)
596+
477597
results = run_unittests(unittests)
478598
unittests, error_messages, stats = process_output('\n'.join(results))
479599

480600
csv_report_path = file_name(CSV_RESULTS_NAME, current_date)
481-
rows = save_as_csv(csv_report_path, unittests, error_messages, stats, current_date)
601+
rows, totals = save_as_csv(csv_report_path, unittests, error_messages, stats, current_date)
482602

483603
missing_modules = process_errors(unittests, error_messages, 'ModuleNotFoundError', get_missing_module)
484604
log("[MISSING MODULES] \n{}", pformat(dict(missing_modules)))
485605

486606
html_report_path = file_name(HTML_RESULTS_NAME, current_date)
487-
save_as_html(html_report_path, rows, missing_modules, current_date)
607+
save_as_html(html_report_path, rows, totals, missing_modules, current_date)
488608

489609
if flags.path:
490610
log("[SAVE] saving results to {} ... ", flags.path)

0 commit comments

Comments
 (0)