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
+
1
38
import csv
2
39
import os
3
40
import re
@@ -228,9 +265,22 @@ class Col(object):
228
265
]
229
266
230
267
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
+
231
281
def save_as_csv (report_path , unittests , error_messages , stats , current_date ):
232
282
rows = []
233
- with open (file_name ( CSV_RESULTS_NAME , current_date ) , 'w' ) as CSV :
283
+ with open (report_path , 'w' ) as CSV :
234
284
totals = {
235
285
Col .NUM_TESTS : 0 ,
236
286
Col .NUM_FAILS : 0 ,
@@ -266,16 +316,23 @@ def save_as_csv(report_path, unittests, error_messages, stats, current_date):
266
316
else :
267
317
total_not_run_at_all += 1
268
318
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
273
330
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 ({
279
336
Col .UNITTEST : 'TOTAL' ,
280
337
Col .NUM_TESTS : totals [Col .NUM_TESTS ],
281
338
Col .NUM_FAILS : totals [Col .NUM_FAILS ],
@@ -284,16 +341,13 @@ def save_as_csv(report_path, unittests, error_messages, stats, current_date):
284
341
Col .NUM_PASSES : totals [Col .NUM_PASSES ],
285
342
Col .PYTHON_ERRORS : 'Could run {0}/{1} unittests ({2:.2f}%). Unittests which pass completely: {3:.2f}%. '
286
343
'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 ])
289
348
})
290
349
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
297
351
298
352
299
353
HTML_TEMPLATE = '''
@@ -370,35 +424,88 @@ def save_as_csv(report_path, unittests, error_messages, stats, current_date):
370
424
'''
371
425
372
426
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
+
374
470
def table (tid , tcols , trows ):
375
- thead = '''
471
+ _thead = '''
376
472
<tr class="text-align: right;">
377
473
<th data-orderable="false"> </th>
378
474
{columns}
379
475
</tr>
380
476
''' .format (columns = '\n ' .join (['<th>{}</th>' .format (c ) for c in tcols ]))
381
477
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 )
383
495
384
- tbody = '\n ' .join ([
496
+ _tbody = '\n ' .join ([
385
497
'<tr class="{cls}"><td>{i}</td>{vals}</tr>' .format (
386
498
cls = 'info' if i % 2 == 0 else '' , i = i ,
387
499
vals = ' ' .join (['<td>{}</td>' .format (format_val (row , k )) for k in tcols ]))
388
500
for i , row in enumerate (trows )])
389
501
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 )
402
509
403
510
scripts = '''
404
511
<script src="https:////cdn.datatables.net/{datatables_version}/js/jquery.dataTables.min.js"></script>
@@ -414,21 +521,25 @@ def table(tid, tcols, trows):
414
521
</script>
415
522
'''
416
523
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 )
428
526
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
+ ])
430
539
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 ])
432
543
433
544
report = HTML_TEMPLATE .format (
434
545
title = 'GraalPython Unittests Stats' ,
@@ -459,6 +570,7 @@ def main(prog, args):
459
570
parser .add_argument ("-v" , "--verbose" , help = "Verbose output." , action = "store_true" )
460
571
parser .add_argument ("-l" , "--limit" , help = "Limit the number of unittests to run." , default = None , type = int )
461
572
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 )
462
574
parser .add_argument ("path" , help = "Path to store the csv output and logs to." , nargs = '?' , default = None )
463
575
464
576
global flags
@@ -473,18 +585,26 @@ def main(prog, args):
473
585
else :
474
586
log ("[INFO] results will not be saved remotely" )
475
587
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
+
477
597
results = run_unittests (unittests )
478
598
unittests , error_messages , stats = process_output ('\n ' .join (results ))
479
599
480
600
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 )
482
602
483
603
missing_modules = process_errors (unittests , error_messages , 'ModuleNotFoundError' , get_missing_module )
484
604
log ("[MISSING MODULES] \n {}" , pformat (dict (missing_modules )))
485
605
486
606
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 )
488
608
489
609
if flags .path :
490
610
log ("[SAVE] saving results to {} ... " , flags .path )
0 commit comments