1# -*- coding: utf-8 -*-
2import xml.etree.ElementTree as ET
3import outer_packages
4import argparse
5import glob
6from pprint import pprint
7import sys, os
8from collections import OrderedDict
9import copy
10import datetime, time
11import traceback
12import yaml
13import subprocess, shlex
14from ansi2html import Ansi2HTMLConverter
15
16converter = Ansi2HTMLConverter(inline = True)
17convert = converter.convert
18
19def ansi2html(text):
20    return convert(text, full = False)
21
22FUNCTIONAL_CATEGORY = 'Functional' # how to display those categories
23ERROR_CATEGORY = 'Error'
24
25
26def try_write(file, text):
27    try:
28        file.write(text)
29    except:
30        try:
31            file.write(text.encode('utf-8'))
32        except:
33            file.write(text.decode('utf-8'))
34
35def pad_tag(text, tag):
36    return '<%s>%s</%s>' % (tag, text, tag)
37
38def mark_string(text, color, condition):
39    if condition:
40        return '<font color=%s><b>%s</b></font>' % (color, text)
41    return text
42
43
44def is_functional_test_name(testname):
45    #if testname.startswith(('platform_', 'misc_methods_', 'vm_', 'payload_gen_', 'pkt_builder_')):
46    #    return True
47    #return False
48    if testname.startswith('functional_tests.'):
49        return True
50    return False
51
52def is_good_status(text):
53    return text in ('Successful', 'Fixed', 'Passed', 'True', 'Pass')
54
55# input: xml element with test result
56# output string: 'error', 'failure', 'skipped', 'passed'
57def get_test_result(test):
58    for child in test.getchildren():
59        if child.tag in ('error', 'failure', 'skipped'):
60            return child.tag
61    return 'passed'
62
63# returns row of table with <th> and <td> columns - key: value
64def add_th_td(key, value):
65    return '<tr><th>%s</th><td>%s</td></tr>\n' % (key, value)
66
67# returns row of table with <td> and <td> columns - key: value
68def add_td_td(key, value):
69    return '<tr><td>%s</td><td>%s</td></tr>\n' % (key, value)
70
71# returns row of table with <th> and <th> columns - key: value
72def add_th_th(key, value):
73    return '<tr><th>%s</th><th>%s</th></tr>\n' % (key, value)
74
75# returns <div> with table of tests under given category.
76# category - string with name of category
77# tests - list of tests, derived from aggregated xml report, changed a little to get easily stdout etc.
78# tests_type - stateful or stateless
79# category_info_dir - folder to search for category info file
80# expanded - bool, false = outputs (stdout etc.) of tests are hidden by CSS
81# brief - bool, true = cut some part of tests outputs (useful for errors section with expanded flag)
82def add_category_of_tests(category, tests, tests_type = None, category_info_dir = None, expanded = False, brief = False):
83    is_actual_category = category not in (FUNCTIONAL_CATEGORY, ERROR_CATEGORY)
84    category_id = '_'.join([category, tests_type]) if tests_type else category
85    category_name = ' '.join([category, tests_type.capitalize()]) if tests_type else category
86    html_output = ''
87    if is_actual_category:
88        html_output += '<br><table class="reference">\n'
89
90        if category_info_dir:
91            category_info_file = '%s/report_%s.info' % (category_info_dir, category)
92            if os.path.exists(category_info_file):
93                with open(category_info_file) as f:
94                    for info_line in f.readlines():
95                        key_value = info_line.split(':', 1)
96                        if key_value[0].strip() in list(trex_info_dict.keys()) + ['User']: # always 'hhaim', no need to show
97                            continue
98                        html_output += add_th_td('%s:' % key_value[0], key_value[1])
99            else:
100                html_output += add_th_td('Info:', 'No info')
101                print('add_category_of_tests: no category info %s' % category_info_file)
102        if tests_type:
103            html_output += add_th_td('Tests type:', tests_type.capitalize())
104        if len(tests):
105            total_duration = 0.0
106            for test in tests:
107                total_duration += float(test.attrib['time'])
108            html_output += add_th_td('Tests duration:', datetime.timedelta(seconds = int(total_duration)))
109        html_output += '</table>\n'
110
111    if not len(tests):
112        return html_output + pad_tag('<br><font color=red>No tests!</font>', 'b')
113    html_output += '<br>\n<table class="reference" width="100%">\n<tr><th align="left">'
114
115    if category == ERROR_CATEGORY:
116        html_output += 'Setup</th><th align="left">Failed tests:'
117    else:
118        html_output += '%s tests:' % category_name
119    html_output += '</th><th align="center">Final Result</th>\n<th align="center">Time (s)</th>\n</tr>\n'
120    for test in tests:
121        functional_test = is_functional_test_name(test.attrib['name'])
122        if functional_test and is_actual_category:
123            continue
124        if category == ERROR_CATEGORY:
125            test_id = ('err_' + test.attrib['classname'] + test.attrib['name']).replace('.', '_')
126        else:
127            test_id = (category_id + test.attrib['name']).replace('.', '_')
128        if expanded:
129            html_output += '<tr>\n<th>'
130        else:
131            html_output += '<tr onclick=tgl_test("%s") class=linktr>\n<td class=linktext>' % test_id
132        if category == ERROR_CATEGORY:
133            html_output += FUNCTIONAL_CATEGORY if functional_test else test.attrib['classname']
134            if expanded:
135                html_output += '</th><td>'
136            else:
137                html_output += '</td><td class=linktext>'
138        html_output += '%s</td>\n<td align="center">' % test.attrib['name']
139        test_result = get_test_result(test)
140        if test_result == 'error':
141            html_output += '<font color="red"><b>ERROR</b></font></td>'
142        elif test_result == 'failure':
143            html_output += '<font color="red"><b>FAILED</b></font></td>'
144        elif test_result == 'skipped':
145            html_output += '<font color="blue"><b>SKIPPED</b></font></td>'
146        else:
147            html_output += '<font color="green"><b>PASSED</b></font></td>'
148        html_output += '<td align="center"> '+ test.attrib['time'] + '</td></tr>'
149
150        result, result_text = test.attrib.get('result', ('', ''))
151        if result_text:
152            start_index_errors_stl = result_text.find('STLError: \n******')
153            if start_index_errors_stl > 0:
154                result_text = result_text[start_index_errors_stl:].strip() # cut traceback
155            start_index_errors = result_text.find('Exception: The test is failed, reasons:')
156            if start_index_errors > 0:
157                result_text = result_text[start_index_errors + 10:].strip() # cut traceback
158            result_text = ansi2html(result_text)
159            result_text = '<b style="color:000080;">%s:</b><br>%s<br><br>' % (result.capitalize(), result_text.replace('\n', '<br>'))
160        stderr = '' if brief and result_text else test.get('stderr', '')
161        if stderr:
162            stderr = ansi2html(stderr)
163            stderr = '<b style="color:000080;"><text color=000080>Stderr</text>:</b><br>%s<br><br>\n' % stderr.replace('\n', '<br>')
164        stdout = '' if brief and result_text else test.get('stdout', '')
165        if stdout:
166            stdout = ansi2html(stdout)
167            if brief: # cut off server logs
168                stdout = stdout.split('>>>>>>>>>>>>>>>', 1)[0]
169            stdout = '<b style="color:000080;">Stdout:</b><br>%s<br><br>\n' % stdout.replace('\n', '<br>')
170
171        html_output += '<tr style="%scolor:603000;" id="%s"><td colspan=%s>' % ('' if expanded else 'display:none;', test_id, 4 if category == ERROR_CATEGORY else 3)
172        if result_text or stderr or stdout:
173            html_output += '%s%s%s</td></tr>' % (result_text, stderr, stdout)
174        else:
175            html_output += '<b style="color:000080;">No output</b></td></tr>'
176
177    html_output += '\n</table>'
178    return html_output
179
180style_css = """
181html {overflow-y:scroll;}
182
183body {
184    font-size:12px;
185    color:#000000;
186    background-color:#ffffff;
187    margin:0px;
188    font-family:verdana,helvetica,arial,sans-serif;
189}
190
191div {width:100%;}
192
193table,th,td,input,textarea {
194    font-size:100%;
195}
196
197table.reference, table.reference_fail {
198    background-color:#ffffff;
199    border:1px solid #c3c3c3;
200    border-collapse:collapse;
201    vertical-align:middle;
202}
203
204table.reference th {
205    background-color:#e5eecc;
206    border:1px solid #c3c3c3;
207    padding:3px;
208}
209
210table.reference_fail th {
211    background-color:#ffcccc;
212    border:1px solid #c3c3c3;
213    padding:3px;
214}
215
216
217table.reference td, table.reference_fail td {
218    border:1px solid #c3c3c3;
219    padding:3px;
220}
221
222a.example {font-weight:bold}
223
224#a:link,a:visited {color:#900B09; background-color:transparent}
225#a:hover,a:active {color:#FF0000; background-color:transparent}
226
227.linktr {
228    cursor: pointer;
229}
230
231.linktext {
232    color:#0000FF;
233    text-decoration: underline;
234}
235"""
236
237
238# main
239if __name__ == '__main__':
240
241    # deal with input args
242    argparser = argparse.ArgumentParser(description='Aggregate test results of from ./reports dir, produces xml, html, mail report.')
243    argparser.add_argument('--input_dir', default='./reports',
244                   help='Directory with xmls/setups info. Filenames: report_<setup name>.xml/report_<setup name>.info')
245    argparser.add_argument('--output_xml', default='./reports/aggregated_tests.xml',
246                   dest = 'output_xmlfile', help='Name of output xml file with aggregated results.')
247    argparser.add_argument('--output_html', default='./reports/aggregated_tests.html',
248                   dest = 'output_htmlfile', help='Name of output html file with aggregated results.')
249    argparser.add_argument('--output_mail', default='./reports/aggregated_tests_mail.html',
250                   dest = 'output_mailfile', help='Name of output html file with aggregated results for mail.')
251    argparser.add_argument('--output_title', default='./reports/aggregated_tests_title.txt',
252                   dest = 'output_titlefile', help='Name of output file to contain title of mail.')
253    argparser.add_argument('--build_status_file', default='./reports/build_status',
254                   dest = 'build_status_file', help='Name of output file to save scenaries build results (should not be wiped).')
255    argparser.add_argument('--last_passed_commit', default='./reports/last_passed_commit',
256                   dest = 'last_passed_commit', help='Name of output file to save last passed commit (should not be wiped).')
257    args = argparser.parse_args()
258
259
260##### get input variables/TRex commit info
261
262    scenario                = os.environ.get('SCENARIO')
263    build_url               = os.environ.get('BUILD_URL')
264    build_id                = os.environ.get('BUILD_NUM')
265    trex_repo               = os.environ.get('TREX_CORE_REPO')
266    last_commit_info_file   = os.environ.get('LAST_COMMIT_INFO')
267    last_commit_branch_file = os.environ.get('LAST_COMMIT_BRANCH')
268    python_ver              = os.environ.get('PYTHON_VER')
269    if not scenario:
270        print('Warning: no environment variable SCENARIO, using default')
271        scenario = 'TRex regression'
272    if not build_url:
273        print('Warning: no environment variable BUILD_URL')
274    if not build_id:
275        print('Warning: no environment variable BUILD_NUM')
276    if not python_ver:
277        print('Warning: no environment variable PYTHON_VER')
278
279    trex_info_dict = OrderedDict()
280    for file in glob.glob('%s/report_*.info' % args.input_dir):
281        with open(file) as f:
282            file_lines = f.readlines()
283            if not len(file_lines):
284                continue # to next file
285            for info_line in file_lines:
286                key_value = info_line.split(':', 1)
287                not_trex_keys = ['Server', 'Router', 'User']
288                if key_value[0].strip() in not_trex_keys:
289                    continue # to next parameters
290                trex_info_dict[key_value[0].strip()] = key_value[1].strip()
291            break
292
293    branch_name = ''
294    if last_commit_branch_file and os.path.exists(last_commit_branch_file):
295        with open(last_commit_branch_file) as f:
296            branch_name = f.read().strip()
297
298    trex_last_commit_info = ''
299    trex_last_commit_hash = trex_info_dict.get('Git SHA')
300    if last_commit_info_file and os.path.exists(last_commit_info_file):
301        with open(last_commit_info_file) as f:
302            trex_last_commit_info = f.read().strip().replace('\n', '<br>\n')
303    elif trex_last_commit_hash and trex_repo:
304        try:
305            command = 'git show %s -s' % trex_last_commit_hash
306            print('Executing: %s' % command)
307            proc = subprocess.Popen(shlex.split(command), stdout = subprocess.PIPE, stderr = subprocess.STDOUT, cwd = trex_repo)
308            (stdout, stderr) = proc.communicate()
309            stdout = stdout.decode('utf-8', errors = 'replace')
310            print('Stdout:\n\t' + stdout.replace('\n', '\n\t'))
311            if stderr or proc.returncode:
312                print('Return code: %s' % proc.returncode)
313            trex_last_commit_info = stdout.replace('\n', '<br>\n')
314        except Exception as e:
315            traceback.print_exc()
316            print('Error getting last commit: %s' % e)
317    else:
318        print('Could not find info about commit!')
319
320##### get xmls: report_<setup name>.xml
321
322    err = []
323    jobs_list = []
324    jobs_file = '%s/jobs_list.info' % args.input_dir
325    if os.path.exists(jobs_file):
326        with open('%s/jobs_list.info' % args.input_dir) as f:
327            for line in f.readlines():
328                line = line.strip()
329                if line:
330                    jobs_list.append(line)
331    else:
332        message = '%s does not exist!' % jobs_file
333        print(message)
334        err.append(message)
335
336##### aggregate results to 1 single tree
337    aggregated_root = ET.Element('testsuite')
338    test_types = ('functional', 'stateful', 'stateless')
339    setups = {}
340    for job in jobs_list:
341        setups[job] = {}
342        for test_type in test_types:
343            xml_file = '%s/report_%s_%s.xml' % (args.input_dir, job, test_type)
344            if not os.path.exists(xml_file):
345                continue
346            if os.path.basename(xml_file) == os.path.basename(args.output_xmlfile):
347                continue
348            setups[job][test_type] = []
349            print('Processing report: %s.%s' % (job, test_type))
350            tree = ET.parse(xml_file)
351            root = tree.getroot()
352            for key, value in root.attrib.items():
353                if key in aggregated_root.attrib and value.isdigit(): # sum total number of failed tests etc.
354                    aggregated_root.attrib[key] = str(int(value) + int(aggregated_root.attrib[key]))
355                else:
356                    aggregated_root.attrib[key] = value
357            tests = root.getchildren()
358            if not len(tests): # there should be tests:
359                message = 'No tests in xml %s' % xml_file
360                print(message)
361                #err.append(message)
362            for test in tests:
363                setups[job][test_type].append(test)
364                test.attrib['name'] = test.attrib['classname'] + '.' + test.attrib['name']
365                test.attrib['classname'] = job
366                aggregated_root.append(test)
367        if not sum([len(x) for x in setups[job].values()]):
368            message = 'No reports from setup %s!' % job
369            print(message)
370            err.append(message)
371            continue
372
373    total_tests_count   = int(aggregated_root.attrib.get('tests', 0))
374    error_tests_count   = int(aggregated_root.attrib.get('errors', 0))
375    failure_tests_count = int(aggregated_root.attrib.get('failures', 0))
376    skipped_tests_count = int(aggregated_root.attrib.get('skip', 0))
377    passed_tests_count  = total_tests_count - error_tests_count - failure_tests_count - skipped_tests_count
378
379    tests_count_string = mark_string('Total: %s' % total_tests_count, 'red', total_tests_count == 0) + ', '
380    tests_count_string += mark_string('Passed: %s' % passed_tests_count, 'red', error_tests_count + failure_tests_count > 0) + ', '
381    tests_count_string += mark_string('Error: %s' % error_tests_count, 'red', error_tests_count > 0) + ', '
382    tests_count_string += mark_string('Failure: %s' % failure_tests_count, 'red', failure_tests_count > 0) + ', '
383    tests_count_string += 'Skipped: %s' % skipped_tests_count
384
385##### save output xml
386
387    print('Writing output file: %s' % args.output_xmlfile)
388    ET.ElementTree(aggregated_root).write(args.output_xmlfile)
389
390
391##### build output html
392    error_tests = []
393    functional_tests = OrderedDict()
394    # categorize and get output of each test
395    for test in aggregated_root.getchildren(): # each test in xml
396        if is_functional_test_name(test.attrib['name']):
397            functional_tests[test.attrib['name']] = test
398        result_tuple = None
399        for child in test.getchildren():        # <system-out>, <system-err>  (<failure>, <error>, <skipped> other: passed)
400#            if child.tag in ('failure', 'error'):
401                #temp = copy.deepcopy(test)
402                #print temp._children
403                #print test._children
404#                error_tests.append(test)
405            if child.tag == 'failure':
406                error_tests.append(test)
407                result_tuple = ('failure', child.text)
408            elif child.tag == 'error':
409                error_tests.append(test)
410                result_tuple = ('error', child.text)
411            elif child.tag == 'skipped':
412                result_tuple = ('skipped', child.text)
413            elif child.tag == 'system-out':
414                test.attrib['stdout'] = child.text
415            elif child.tag == 'system-err':
416                test.attrib['stderr'] = child.text
417        if result_tuple:
418            test.attrib['result'] = result_tuple
419
420    html_output = '''\
421<html>
422<head>
423<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
424<style type="text/css">
425'''
426    html_output += style_css
427    html_output +='''
428</style>
429</head>
430
431<body>
432<table class="reference">
433'''
434    if scenario:
435        html_output += add_th_td('Scenario:', scenario.capitalize())
436    if python_ver:
437        html_output += add_th_td('Python:', python_ver)
438    start_time_file = '%s/start_time.info' % args.input_dir
439    if os.path.exists(start_time_file):
440        with open(start_time_file) as f:
441            start_time = int(f.read())
442        total_time = int(time.time()) - start_time
443        html_output += add_th_td('Regression start:', datetime.datetime.fromtimestamp(start_time).strftime('%d/%m/%Y %H:%M'))
444        html_output += add_th_td('Regression duration:', datetime.timedelta(seconds = total_time))
445    html_output += add_th_td('Tests count:', tests_count_string)
446    for key in trex_info_dict:
447        if key == 'Git SHA':
448            continue
449        html_output += add_th_td(key, trex_info_dict[key])
450    if trex_last_commit_info:
451        html_output += add_th_td('Last commit:', trex_last_commit_info)
452    html_output += '</table><br>\n'
453    if err:
454        html_output += '<font color=red>%s<font><br><br>\n' % '\n<br>'.join(err)
455
456#<table style="width:100%;">
457#    <tr>
458#        <td>Summary:</td>\
459#'''
460    #passed_quantity = len(result_types['passed'])
461    #failed_quantity = len(result_types['failed'])
462    #error_quantity = len(result_types['error'])
463    #skipped_quantity = len(result_types['skipped'])
464
465    #html_output += '<td>Passed: %s</td>' % passed_quantity
466    #html_output += '<td>Failed: %s</td>' % (pad_tag(failed_quantity, 'b') if failed_quantity else '0')
467    #html_output += '<td>Error: %s</td>' % (pad_tag(error_quantity, 'b') if error_quantity else '0')
468    #html_output += '<td>Skipped: %s</td>' % (pad_tag(skipped_quantity, 'b') if skipped_quantity else '0')
469#    html_output += '''
470#    </tr>
471#</table>'''
472
473    category_arr = [FUNCTIONAL_CATEGORY, ERROR_CATEGORY]
474
475# Adding buttons
476    # Error button
477    if len(error_tests):
478        html_output += '\n<button onclick=tgl_cat("cat_tglr_{error}")>{error}</button>'.format(error = ERROR_CATEGORY)
479    # Setups buttons
480    for category in sorted(setups.keys()):
481        category_arr.append(category)
482        html_output += '\n<button onclick=tgl_cat("cat_tglr_%s")>%s</button>' % (category_arr[-1], category)
483    # Functional buttons
484    if len(functional_tests):
485        html_output += '\n<button onclick=tgl_cat("cat_tglr_%s")>%s</button>' % (FUNCTIONAL_CATEGORY, FUNCTIONAL_CATEGORY)
486
487# Adding tests
488    # Error tests
489    if len(error_tests):
490        html_output += '<div style="display:block;" id="cat_tglr_%s">' % ERROR_CATEGORY
491        html_output += add_category_of_tests(ERROR_CATEGORY, error_tests)
492        html_output += '</div>'
493    # Setups tests
494    for category, tests in setups.items():
495        html_output += '<div style="display:none;" id="cat_tglr_%s">' % category
496        if 'stateful' in tests:
497            html_output += add_category_of_tests(category, tests['stateful'], 'stateful', category_info_dir=args.input_dir)
498        if 'stateless' in tests:
499            html_output += add_category_of_tests(category, tests['stateless'], 'stateless', category_info_dir=(None if 'stateful' in tests else args.input_dir))
500        html_output += '</div>'
501    # Functional tests
502    if len(functional_tests):
503        html_output += '<div style="display:none;" id="cat_tglr_%s">' % FUNCTIONAL_CATEGORY
504        html_output += add_category_of_tests(FUNCTIONAL_CATEGORY, functional_tests.values())
505        html_output += '</div>'
506
507    html_output += '\n\n<script type="text/javascript">\n    var category_arr = %s\n' % ['cat_tglr_%s' % x for x in category_arr]
508    html_output += '''
509    function tgl_cat(id)
510        {
511        for(var i=0; i<category_arr.length; i++)
512            {
513            var e = document.getElementById(category_arr[i]);
514            if (id == category_arr[i])
515                {
516                if(e.style.display == 'block')
517                    e.style.display = 'none';
518                else
519                    e.style.display = 'block';
520                }
521            else
522                {
523                if (e) e.style.display = 'none';
524                }
525            }
526        }
527    function tgl_test(id)
528        {
529        var e = document.getElementById(id);
530        if(e.style.display == 'table-row')
531            e.style.display = 'none';
532        else
533            e.style.display = 'table-row';
534        }
535</script>
536</body>
537</html>\
538'''
539
540# save html
541    with open(args.output_htmlfile, 'w') as f:
542        print('Writing output file: %s' % args.output_htmlfile)
543        try_write(f, html_output)
544    html_output = None
545
546# mail report (only error tests, expanded)
547
548    mail_output = '''\
549<html>
550<head>
551<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
552<style type="text/css">
553'''
554    mail_output += style_css
555    mail_output +='''
556</style>
557</head>
558
559<body>
560<table class="reference">
561'''
562    if scenario:
563        mail_output += add_th_td('Scenario:', scenario.capitalize())
564    if python_ver:
565        mail_output += add_th_td('Python:', python_ver)
566    if build_url:
567        mail_output += add_th_td('Full HTML report:', '<a class="example" href="%s/HTML_Report">link</a>' % build_url)
568    start_time_file = '%s/start_time.info' % args.input_dir
569    if os.path.exists(start_time_file):
570        with open(start_time_file) as f:
571            start_time = int(f.read())
572        total_time = int(time.time()) - start_time
573        mail_output += add_th_td('Regression start:', datetime.datetime.fromtimestamp(start_time).strftime('%d/%m/%Y %H:%M'))
574        mail_output += add_th_td('Regression duration:', datetime.timedelta(seconds = total_time))
575    mail_output += add_th_td('Tests count:', tests_count_string)
576    for key in trex_info_dict:
577        if key == 'Git SHA':
578            continue
579        mail_output += add_th_td(key, trex_info_dict[key])
580
581    if trex_last_commit_info:
582        mail_output += add_th_td('Last commit:', trex_last_commit_info)
583    mail_output += '</table><br>\n<table width=100%><tr><td>\n'
584
585    for category in setups.keys():
586        failing_category = False
587        for test in error_tests:
588            if test.attrib['classname'] == category:
589                failing_category = True
590        if failing_category or not len(setups[category]) or not sum([len(x) for x in setups[category]]):
591            mail_output += '<table class="reference_fail" align=left style="Margin-bottom:10;Margin-right:10;">\n'
592        else:
593            mail_output += '<table class="reference" align=left style="Margin-bottom:10;Margin-right:10;">\n'
594        mail_output += add_th_th('Setup:', pad_tag(category.replace('.', '/'), 'b'))
595        category_info_file = '%s/report_%s.info' % (args.input_dir, category.replace('.', '_'))
596        if os.path.exists(category_info_file):
597            with open(category_info_file) as f:
598                for info_line in f.readlines():
599                    key_value = info_line.split(':', 1)
600                    if key_value[0].strip() in list(trex_info_dict.keys()) + ['User']: # always 'hhaim', no need to show
601                        continue
602                    mail_output += add_th_td('%s:' % key_value[0].strip(), key_value[1].strip())
603        else:
604            mail_output += add_th_td('Info:', 'No info')
605        mail_output += '</table>\n'
606    mail_output += '</td></tr></table>\n'
607
608    # Error tests
609    if len(error_tests) or err:
610        if err:
611            mail_output += '<font color=red>%s<font>' % '\n<br>'.join(err)
612        if len(error_tests) > 5:
613            mail_output += '\n<font color=red>More than 5 failed tests, showing brief output.<font>\n<br>'
614            # show only brief version (cut some info)
615            mail_output += add_category_of_tests(ERROR_CATEGORY, error_tests, expanded=True, brief=True)
616        else:
617            mail_output += add_category_of_tests(ERROR_CATEGORY, error_tests, expanded=True)
618    else:
619        mail_output += u'<table><tr style="font-size:120;color:green;font-family:arial"><td>☺</td><td style="font-size:20">All passed.</td></tr></table>\n'
620    mail_output += '\n</body>\n</html>'
621
622##### save outputs
623
624
625# mail content
626    with open(args.output_mailfile, 'w') as f:
627        print('Writing output file: %s' % args.output_mailfile)
628        try_write(f, mail_output)
629
630# build status
631    category_dict_status = {}
632    if os.path.exists(args.build_status_file):
633        print('Reading: %s' % args.build_status_file)
634        with open(args.build_status_file, 'r') as f:
635            try:
636                category_dict_status = yaml.safe_load(f.read())
637            except Exception as e:
638                print('Error during YAML load: %s' % e)
639        if type(category_dict_status) is not dict:
640            print('%s is corrupt, truncating' % args.build_status_file)
641            category_dict_status = {}
642
643    last_status = category_dict_status.get(scenario, 'Successful') # assume last is passed if no history
644    if err or len(error_tests): # has fails
645        exit_status = 1
646        if is_good_status(last_status):
647            current_status = 'Failure'
648        else:
649            current_status = 'Still Failing'
650    else:
651        exit_status = 0
652        if is_good_status(last_status):
653            current_status = 'Successful'
654        else:
655            current_status = 'Fixed'
656    category_dict_status[scenario] = current_status
657
658    with open(args.build_status_file, 'w') as f:
659        print('Writing output file: %s' % args.build_status_file)
660        yaml.dump(category_dict_status, f)
661
662# last successful commit
663    if (current_status in ('Successful', 'Fixed')) and trex_last_commit_hash and len(jobs_list) > 0 and scenario == 'nightly':
664        with open(args.last_passed_commit, 'w') as f:
665            print('Writing output file: %s' % args.last_passed_commit)
666            try_write(f, trex_last_commit_hash)
667
668# mail title
669    mailtitle_output = scenario.capitalize()
670    if branch_name:
671        mailtitle_output += ' (%s)' % branch_name
672    if build_id:
673        mailtitle_output += ' - Build #%s' % build_id
674    mailtitle_output += ' - %s!' % current_status
675
676    with open(args.output_titlefile, 'w') as f:
677        print('Writing output file: %s' % args.output_titlefile)
678        try_write(f, mailtitle_output)
679
680# exit
681    print('Status: %s' % current_status)
682    sys.exit(exit_status)
683