1#!/usr/bin/env python3
2
3r"""
4Use robot framework API to extract test result data from output.xml generated
5by robot tests. For more information on the Robot Framework API, see
6http://robot-framework.readthedocs.io/en/3.0/autodoc/robot.result.html
7"""
8
9import sys
10import os
11import getopt
12import csv
13import robot.errors
14import re
15import stat
16import datetime
17
18from robot.api import ExecutionResult
19from robot.result.visitor import ResultVisitor
20from xml.etree import ElementTree
21
22from gen_arg import *
23from gen_print import *
24from gen_valid import *
25
26# Remove the python library path to restore with local project path later.
27save_path_0 = sys.path[0]
28del sys.path[0]
29sys.path.append(os.path.join(os.path.dirname(__file__), "../../lib"))
30
31# Restore sys.path[0].
32sys.path.insert(0, save_path_0)
33
34
35this_program = sys.argv[0]
36info = " For more information:  " + this_program + '  -h'
37if len(sys.argv) == 1:
38    print(info)
39    sys.exit(1)
40
41
42parser = argparse.ArgumentParser(
43    usage=info,
44    description="%(prog)s uses a robot framework API to extract test result\
45    data from output.xml generated by robot tests. For more information on the\
46    Robot Framework API, see\
47    http://robot-framework.readthedocs.io/en/3.0/autodoc/robot.result.html",
48    formatter_class=argparse.ArgumentDefaultsHelpFormatter,
49    prefix_chars='-+')
50
51parser.add_argument(
52    '--source',
53    '-s',
54    help='The output.xml robot test result file path.  This parameter is \
55          required.')
56
57parser.add_argument(
58    '--dest',
59    '-d',
60    help='The directory path where the generated .csv files will go.  This \
61          parameter is required.')
62
63parser.add_argument(
64    '--version_id',
65    help='Driver version of openbmc firmware which was used during test,\
66          e.g. "v2.1-215-g6e7eacb".  This parameter is required.')
67
68parser.add_argument(
69    '--platform',
70    help='OpenBMC platform which was used during test,\
71          e.g. "Witherspoon".  This parameter is required.')
72
73parser.add_argument(
74    '--level',
75    help='OpenBMC release level which was used during test,\
76          e.g. "Master", "OBMC920".  This parameter is required.')
77
78parser.add_argument(
79    '--test_phase',
80    help='Name of testing phase, e.g. "DVT", "SVT", etc.\
81          This parameter is optional.',
82    default="FVT")
83
84parser.add_argument(
85    '--subsystem',
86    help='Name of the subsystem, e.g. "OPENBMC" etc.\
87          This parameter is optional.',
88    default="OPENBMC")
89
90parser.add_argument(
91    '--processor',
92    help='Name of processor, e.g. "P9". This parameter is optional.',
93    default="OPENPOWER")
94
95
96# Populate stock_list with options we want.
97stock_list = [("test_mode", 0), ("quiet", 0), ("debug", 0)]
98
99
100def exit_function(signal_number=0,
101                  frame=None):
102    r"""
103    Execute whenever the program ends normally or with the signals that we
104    catch (i.e. TERM, INT).
105    """
106
107    dprint_executing()
108
109    dprint_var(signal_number)
110
111    qprint_pgm_footer()
112
113
114def signal_handler(signal_number,
115                   frame):
116    r"""
117    Handle signals.  Without a function to catch a SIGTERM or SIGINT, the
118    program would terminate immediately with return code 143 and without
119    calling the exit_function.
120    """
121
122    # Our convention is to set up exit_function with atexit.register() so
123    # there is no need to explicitly call exit_function from here.
124
125    dprint_executing()
126
127    # Calling exit prevents us from returning to the code that was running
128    # when the signal was received.
129    exit(0)
130
131
132def validate_parms():
133    r"""
134    Validate program parameters, etc.  Return True or False (i.e. pass/fail)
135    accordingly.
136    """
137
138    if not valid_file_path(source):
139        return False
140
141    if not valid_dir_path(dest):
142        return False
143
144    gen_post_validation(exit_function, signal_handler)
145
146    return True
147
148
149def parse_output_xml(xml_file_path, csv_dir_path, version_id, platform, level,
150                     test_phase, processor):
151    r"""
152    Parse the robot-generated output.xml file and extract various test
153    output data. Put the extracted information into a csv file in the "dest"
154    folder.
155
156    Description of argument(s):
157    xml_file_path                   The path to a Robot-generated output.xml
158                                    file.
159    csv_dir_path                    The path to the directory that is to
160                                    contain the .csv files generated by
161                                    this function.
162    version_id                      Version of the openbmc firmware
163                                    (e.g. "v2.1-215-g6e7eacb").
164    platform                        Platform of the openbmc system.
165    level                           Release level of the OpenBMC system
166                                    (e.g. "Master").
167    """
168
169    # Initialize tallies
170    total_critical_tc = 0
171    total_critical_passed = 0
172    total_critical_failed = 0
173    total_non_critical_tc = 0
174    total_non_critical_passed = 0
175    total_non_critical_failed = 0
176
177    result = ExecutionResult(xml_file_path)
178    result.configure(stat_config={'suite_stat_level': 2,
179                                  'tag_stat_combine': 'tagANDanother'})
180
181    stats = result.statistics
182    print("--------------------------------------")
183    try:
184        total_critical_tc = stats.total.critical.passed + stats.total.critical.failed
185        total_critical_passed = stats.total.critical.passed
186        total_critical_failed = stats.total.critical.failed
187    except AttributeError:
188        pass
189
190    try:
191        total_non_critical_tc = stats.total.passed + stats.total.failed
192        total_non_critical_passed = stats.total.passed
193        total_non_critical_failed = stats.total.failed
194    except AttributeError:
195        pass
196
197    print("Total Test Count:\t %d" % (total_non_critical_tc + total_critical_tc))
198
199    print("Total Critical Test Failed:\t %d" % total_critical_failed)
200    print("Total Critical Test Passed:\t %d" % total_critical_passed)
201    print("Total Non-Critical Test Failed:\t %d" % total_non_critical_failed)
202    print("Total Non-Critical Test Passed:\t %d" % total_non_critical_passed)
203    print("Test Start Time:\t %s" % result.suite.starttime)
204    print("Test End Time:\t\t %s" % result.suite.endtime)
205    print("--------------------------------------")
206
207    # Use ResultVisitor object and save off the test data info
208    class TestResult(ResultVisitor):
209        def __init__(self):
210            self.testData = []
211
212        def visit_test(self, test):
213            self.testData += [test]
214
215    collectDataObj = TestResult()
216    result.visit(collectDataObj)
217
218    # Write the result statistics attributes to CSV file
219    l_csvlist = []
220
221    # Default Test data
222    l_test_type = test_phase
223
224    l_pse_rel = 'Master'
225    if level:
226        l_pse_rel = level
227
228    l_env = 'HW'
229    l_proc = processor
230    l_platform_type = ""
231    l_func_area = ""
232
233    # System data from XML meta data
234    # l_system_info = get_system_details(xml_file_path)
235
236    # First let us try to collect information from keyboard input
237    # If keyboard input cannot give both information, then find from xml file.
238    if version_id and platform:
239        l_driver = version_id
240        l_platform_type = platform
241        print("BMC Version_id:%s" % version_id)
242        print("BMC Platform:%s" % platform)
243    else:
244        # System data from XML meta data
245        l_system_info = get_system_details(xml_file_path)
246        l_driver = l_system_info[0]
247        l_platform_type = l_system_info[1]
248
249    # Driver version id and platform are mandatorily required for CSV file
250    # generation. If any one is not avaulable, exit CSV file generation
251    # process.
252    if l_driver and l_platform_type:
253        print("Driver and system info set.")
254    else:
255        print("Both driver and system info need to be set.\
256                CSV file is not generated.")
257        sys.exit()
258
259    # Default header
260    l_header = ['test_start', 'test_end', 'subsys', 'test_type',
261                'test_result', 'test_name', 'pse_rel', 'driver',
262                'env', 'proc', 'platform_type', 'test_func_area']
263
264    l_csvlist.append(l_header)
265
266    # Generate CSV file onto the path with current time stamp
267    l_base_dir = csv_dir_path
268    l_timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H-%M-%S")
269    # Example: 2017-02-20-08-47-22_Witherspoon.csv
270    l_csvfile = l_base_dir + l_timestamp + "_" + l_platform_type + ".csv"
271
272    print("Writing data into csv file:%s" % l_csvfile)
273
274    for testcase in collectDataObj.testData:
275        # Functional Area: Suite Name
276        # Test Name: Test Case Name
277        l_func_area = str(testcase.parent).split(' ', 1)[1]
278        l_test_name = str(testcase)
279
280        # Test Result pass=0 fail=1
281        if testcase.status == 'PASS':
282            l_test_result = 0
283        else:
284            l_test_result = 1
285
286        # Format datetime from robot output.xml to "%Y-%m-%d-%H-%M-%S"
287        l_stime = xml_to_csv_time(testcase.starttime)
288        l_etime = xml_to_csv_time(testcase.endtime)
289        # Data Sequence: test_start,test_end,subsys,test_type,
290        #                test_result,test_name,pse_rel,driver,
291        #                env,proc,platform_type,test_func_area,
292        l_data = [l_stime, l_etime, subsystem, l_test_type, l_test_result,
293                  l_test_name, l_pse_rel, l_driver, l_env, l_proc,
294                  l_platform_type, l_func_area]
295        l_csvlist.append(l_data)
296
297    # Open the file and write to the CSV file
298    l_file = open(l_csvfile, "w")
299    l_writer = csv.writer(l_file, lineterminator='\n')
300    l_writer.writerows(l_csvlist)
301    l_file.close()
302    # Set file permissions 666.
303    perm = stat.S_IRUSR + stat.S_IWUSR + stat.S_IRGRP + stat.S_IWGRP + stat.S_IROTH + stat.S_IWOTH
304    os.chmod(l_csvfile, perm)
305
306
307def xml_to_csv_time(xml_datetime):
308    r"""
309    Convert the time from %Y%m%d %H:%M:%S.%f format to %Y-%m-%d-%H-%M-%S format
310    and return it.
311
312    Description of argument(s):
313    datetime                        The date in the following format: %Y%m%d
314                                    %H:%M:%S.%f (This is the format
315                                    typically found in an XML file.)
316
317    The date returned will be in the following format: %Y-%m-%d-%H-%M-%S
318    """
319
320    # 20170206 05:05:19.342
321    l_str = datetime.datetime.strptime(xml_datetime, "%Y%m%d %H:%M:%S.%f")
322    # 2017-02-06-05-05-19
323    l_str = l_str.strftime("%Y-%m-%d-%H-%M-%S")
324    return str(l_str)
325
326
327def get_system_details(xml_file_path):
328    r"""
329    Get the system data from output.xml generated by robot and return it.
330    The list returned will be in the following order: [driver,platform]
331
332    Description of argument(s):
333    xml_file_path                   The relative or absolute path to the
334                                    output.xml file.
335    """
336
337    bmc_version_id = ""
338    bmc_platform = ""
339    with open(xml_file_path, 'rt') as output:
340        tree = ElementTree.parse(output)
341
342    for node in tree.iter('msg'):
343        # /etc/os-release output is logged in the XML as msg
344        # Example: ${output} = VERSION_ID="v1.99.2-71-gbc49f79"
345        if '${output} = VERSION_ID=' in node.text:
346            # Get BMC version (e.g. v1.99.1-96-g2a46570)
347            bmc_version_id = str(node.text.split("VERSION_ID=")[1])[1:-1]
348
349        # Platform is logged in the XML as msg.
350        # Example: ${bmc_model} = Witherspoon BMC
351        if '${bmc_model} = ' in node.text:
352            bmc_platform = node.text.split(" = ")[1]
353
354    print_vars(bmc_version_id, bmc_platform)
355    return [str(bmc_version_id), str(bmc_platform)]
356
357
358def main():
359
360    if not gen_get_options(parser, stock_list):
361        return False
362
363    if not validate_parms():
364        return False
365
366    qprint_pgm_header()
367
368    parse_output_xml(source, dest, version_id, platform, level,
369                     test_phase, processor)
370
371    return True
372
373
374# Main
375
376if not main():
377    exit(1)
378