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+
138import csv
239import os
340import 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+
231281def 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
299353HTML_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"> </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"> </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"> </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"> </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> <span class="text-muted">count: {}</span></li>' .format (name , cnt )
524+ missing_modules_info = ul ('missing modules' , [
525+ '<b>{}</b> <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