1#! python3
2# ==========================================
3#   Fork from Unity Project - A Test Framework for C
4#   Pull request on Gerrit in progress, the objective of this file is to be deleted when official Unity deliveries
5#   include that modification
6#   Copyright (c) 2015 Alexander Mueller / XelaRellum@web.de
7#   [Released under MIT License. Please refer to license.txt for details]
8# ==========================================
9import sys
10import os
11from glob import glob
12import argparse
13
14from pyparsing import *
15from junit_xml import TestSuite, TestCase
16
17
18class UnityTestSummary:
19    def __init__(self):
20        self.report = ''
21        self.total_tests = 0
22        self.failures = 0
23        self.ignored = 0
24        self.targets = 0
25        self.root = None
26        self.output = None
27        self.test_suites = dict()
28
29    def run(self):
30        # Clean up result file names
31        results = []
32        for target in self.targets:
33            results.append(target.replace('\\', '/'))
34
35        # Dig through each result file, looking for details on pass/fail:
36        for result_file in results:
37            lines = list(map(lambda line: line.rstrip(), open(result_file, "r").read().split('\n')))
38            if len(lines) == 0:
39                raise Exception("Empty test result file: %s" % result_file)
40
41            # define an expression for your file reference
42            entry_one = Combine(
43                oneOf(list(alphas)) + ':/' +
44                Word(alphanums + '_-./'))
45
46            entry_two = Word(printables + ' ', excludeChars=':')
47            entry = entry_one | entry_two
48
49            delimiter = Literal(':').suppress()
50            # Format of a result line is `[file_name]:line:test_name:RESULT[:msg]`
51            tc_result_line = Group(ZeroOrMore(entry.setResultsName('tc_file_name'))
52                + delimiter + entry.setResultsName('tc_line_nr')
53                + delimiter + entry.setResultsName('tc_name')
54                + delimiter + entry.setResultsName('tc_status') +
55                Optional(delimiter + entry.setResultsName('tc_msg'))).setResultsName("tc_line")
56
57            eol = LineEnd().suppress()
58            sol = LineStart().suppress()
59            blank_line = sol + eol
60
61            # Format of the summary line is `# Tests # Failures # Ignored`
62            tc_summary_line = Group(Word(nums).setResultsName("num_of_tests") + "Tests" + Word(nums).setResultsName(
63                "num_of_fail") + "Failures" + Word(nums).setResultsName("num_of_ignore") + "Ignored").setResultsName(
64                "tc_summary")
65            tc_end_line = Or(Literal("FAIL"), Literal('Ok')).setResultsName("tc_result")
66
67            # run it and see...
68            pp1 = tc_result_line | Optional(tc_summary_line | tc_end_line)
69            pp1.ignore(blank_line | OneOrMore("-"))
70
71            result = list()
72            for l in lines:
73                result.append((pp1.parseString(l)).asDict())
74            # delete empty results
75            result = filter(None, result)
76
77            tc_list = list()
78            for r in result:
79                if 'tc_line' in r:
80                    tmp_tc_line = r['tc_line']
81
82                    # get only the file name which will be used as the classname
83                    if 'tc_file_name' in tmp_tc_line:
84                        file_name = tmp_tc_line['tc_file_name'].split('\\').pop().split('/').pop().rsplit('.', 1)[0]
85                    else:
86                        file_name = result_file.strip("./")
87                    tmp_tc = TestCase(name=tmp_tc_line['tc_name'], classname=file_name)
88                    if 'tc_status' in tmp_tc_line:
89                        if str(tmp_tc_line['tc_status']) == 'IGNORE':
90                            if 'tc_msg' in tmp_tc_line:
91                                tmp_tc.add_skipped_info(message=tmp_tc_line['tc_msg'],
92                                                        output=r'[File]={0}, [Line]={1}'.format(
93                                                            tmp_tc_line['tc_file_name'], tmp_tc_line['tc_line_nr']))
94                            else:
95                                tmp_tc.add_skipped_info(message=" ")
96                        elif str(tmp_tc_line['tc_status']) == 'FAIL':
97                            if 'tc_msg' in tmp_tc_line:
98                                tmp_tc.add_failure_info(message=tmp_tc_line['tc_msg'],
99                                                        output=r'[File]={0}, [Line]={1}'.format(
100                                                            tmp_tc_line['tc_file_name'], tmp_tc_line['tc_line_nr']))
101                            else:
102                                tmp_tc.add_failure_info(message=" ")
103
104                    tc_list.append((str(result_file), tmp_tc))
105
106            for k, v in tc_list:
107                try:
108                    self.test_suites[k].append(v)
109                except KeyError:
110                    self.test_suites[k] = [v]
111        ts = []
112        for suite_name in self.test_suites:
113            ts.append(TestSuite(suite_name, self.test_suites[suite_name]))
114
115        with open(self.output, 'w') as f:
116            TestSuite.to_file(f, ts, prettyprint='True', encoding='utf-8')
117
118        return self.report
119
120    def set_targets(self, target_array):
121        self.targets = target_array
122
123    def set_root_path(self, path):
124        self.root = path
125
126    def set_output(self, output):
127        self.output = output
128
129
130if __name__ == '__main__':
131    uts = UnityTestSummary()
132    parser = argparse.ArgumentParser(description=
133        """Takes as input the collection of *.testpass and *.testfail result
134        files, and converts them to a JUnit formatted XML.""")
135    parser.add_argument('targets_dir', metavar='result_file_directory',
136                        type=str, nargs='?', default='./',
137                        help="""The location of your results files.
138                        Defaults to current directory if not specified.""")
139    parser.add_argument('root_path', nargs='?',
140                        default='os.path.split(__file__)[0]',
141                        help="""Helpful for producing more verbose output if
142                        using relative paths.""")
143    parser.add_argument('--output', '-o', type=str, default="result.xml",
144                        help="""The name of the JUnit-formatted file (XML).""")
145    args = parser.parse_args()
146
147    if args.targets_dir[-1] != '/':
148        args.targets_dir+='/'
149    targets = list(map(lambda x: x.replace('\\', '/'), glob(args.targets_dir + '*.test*')))
150    if len(targets) == 0:
151        raise Exception("No *.testpass or *.testfail files found in '%s'" % args.targets_dir)
152    uts.set_targets(targets)
153
154    # set the root path
155    uts.set_root_path(args.root_path)
156
157    # set output
158    uts.set_output(args.output)
159
160    # run the summarizer
161    print(uts.run())
162