Passed
Push — feature/empty_name_assert ( c21e31...0560d9 )
by srz
73:15
created

iutest_compile_error_test.dump_msg()   B

Complexity

Conditions 6

Size

Total Lines 17
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nop 1
dl 0
loc 17
rs 8.6666
c 0
b 0
f 0
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
#
4
# iutest_compiler_error_test.py
5
#
6
# Copyright (C) 2015-2016, Takazumi Shirayanagi
7
# This software is released under the new BSD License,
8
# see LICENSE
9
#
10
11
import os
12
import sys
13
import re
14
import copy
15
16
from argparse import ArgumentParser
17
from argparse import FileType
18
from subprocess import Popen, PIPE, STDOUT
19
20
21
class ErrorMessage:
22
    file = ""
23
    line = 0
24
    type = ""
25
    message = ""
26
    parent = None
27
    child = None
28
    checked = False
29
    target = False
30
    expansion = False
31
    root_is_expansion = False
32
    included_from = []
33
34
    def set_type(self, str):
35
        s = str.strip()
36
        if s in {"error", u"エラー"}:
37
            self.type = "error"
38
        elif s in {"note", u"備考"}:
39
            self.type = "note"
40
        elif s in {"warning", u"警告"}:
41
            self.type = "warning"
42
        if s in {"error", "エラー"}:
43
            self.type = "error"
44
        elif s in {"note", "備考"}:
45
            self.type = "note"
46
        elif s in {"warning", "警告"}:
47
            self.type = "warning"
48
        else:
49
            self.type = s
50
51
    def is_error(self):
52
        if self.type == "error":
53
            return True
54
        return False
55
56
    def is_note(self):
57
        if self.type == "note":
58
            return True
59
        return False
60
61
    def is_iutest(self):
62
        if self.is_note() and self.message.find("IUTEST_TEST_COMPILEERROR") != -1:
63
            return True
64
        return False
65
66
    def is_type_none(self):
67
        if not self.type:
68
            return True
69
        return False
70
71
    def is_warning(self):
72
        if self.type == "warning":
73
            return True
74
        return False
75
76
    def is_type_root(self):
77
        if self.is_warning():
78
            return True
79
        if self.is_error():
80
            return True
81
        return False
82
83
    def has_error(self):
84
        if self.type == "error":
85
            return True
86
        elif self.parent and self.parent.has_error_parent():
87
            return True
88
        elif self.child and self.child.has_error_child():
89
            return True
90
        return False
91
92
    def has_error_parent(self):
93
        if self.type == "error":
94
            return True
95
        elif self.parent:
96
            return self.parent.has_error_parent()
97
        return False
98
99
    def has_error_child(self):
100
        if self.type == "error":
101
            return True
102
        elif self.child:
103
            return self.child.has_error_child()
104
        return False
105
106
    def is_checked(self):
107
        if self.checked:
108
            return True
109
        elif self.parent:
110
            return self.parent.is_checked()
111
        return False
112
113
    def is_expansion(self):
114
        return self.expansion
115
116
    def is_tail(self):
117
        if self.child:
118
            return False
119
        return True
120
121
    def get_error(self):
122
        if self.type == "error":
123
            return self
124
        if self.parent:
125
            e = self.parent.get_error_parent()
126
            if e:
127
                return e
128
        if self.child:
129
            e = self.child.get_error_child()
130
            if e:
131
                return e
132
        return None
133
134
    def get_error_parent(self):
135
        if self.type == "error":
136
            return self
137
        elif self.parent:
138
            return self.parent.get_error_parent()
139
        return None
140
141
    def get_error_child(self):
142
        if self.type == "error":
143
            return self
144
        elif self.child:
145
            return self.child.get_error_child()
146
        return None
147
148
    def get_root_msg(self):
149
        if self.parent:
150
            return self.parent.get_root_msg()
151
        return self
152
153
    def get_source_msg(self):
154
        root = self.get_root_msg()
155
        if self.root_is_expansion:
156
            while root.child:
157
                if root.child.expansion:
158
                    root = root.child
159
                else:
160
                    break
161
        return root
162
163
164
class ExpectMessage:
165
    def __init__(self, msg, expect, expression):
166
        self.msg = msg
167
        self.expect = expect
168
        self.expression = expression
169
170
171
format_gcc = True
172
color_prompt = False
173
174
175
# command line option
176
def parse_command_line():
177
    parser = ArgumentParser()
178
    parser.add_argument(
179
        '-v',
180
        '--version',
181
        action='version',
182
        version=u'%(prog)s version 0.4'
183
    )
184
    parser.add_argument(
185
        '-c',
186
        '--compiler',
187
        help='set compiler.',
188
        default='gcc'
189
    )
190
    parser.add_argument(
191
        '--passthrough',
192
        action='store_true',
193
        help='print input message.'
194
    )
195
    parser.add_argument(
196
        '--verbose',
197
        action='store_true',
198
        help='print input message when failed.'
199
    )
200
    parser.add_argument(
201
        '--debug',
202
        action='store_true',
203
        help='debug.'
204
    )
205
    parser.add_argument(
206
        '--command',
207
        help='execute command.',
208
        default=None
209
    )
210
    if sys.version_info[0] >= 3:
211
        parser.add_argument(
212
            '-i',
213
            '--infile',
214
            type=FileType('r', encoding='UTF-8'),
215
            help='compiler stdout.',
216
            default=sys.stdin
217
        )
218
    else:
219
        parser.add_argument(
220
            '-i',
221
            '--infile',
222
            type=FileType('r'),
223
            help='compiler stdout.',
224
            default=sys.stdin
225
        )
226
227
    options = parser.parse_args()
228
    return options
229
230
231
def parse_gcc_clang(options, f, r_expansion, note_is_child, root_is_expansion):
232
    re_fatal = re.compile(r'(\S+)\s*:\s*fatal\s*error\s*.*')
233
234
    class rmessage:
235
        re_file_col = re.compile(r'(\S+):(\d+):(\d+):(.*)')
236
        re_file = re.compile(r'(\S+):(\d+):(.*)')
237
        re_infile = re.compile(r'In file included from (\S+):(\d+):(?:\d+|)(.*)')
238
        re_ininst = re.compile(r'(\S+):\s* (In instantiation of .*)')
239
240
        def __init__(self):
241
            self.m0 = None
242
            self.m1 = None
243
            self.m2 = None
244
            self.m3 = None
245
246
        def match(self, line):
247
            self.m0 = self.re_file_col.match(line)
248
            if self.m0:
249
                return True
250
            self.m1 = self.re_file.match(line)
251
            if self.m1:
252
                return True
253
            self.m2 = self.re_infile.match(line)
254
            if self.m2:
255
                return True
256
            self.m3 = self.re_ininst.match(line)
257
            if self.m3:
258
                return True
259
            return False
260
261
        def file(self):
262
            if self.m0:
263
                return self.m0.group(1)
264
            if self.m1:
265
                return self.m1.group(1)
266
            if self.m2:
267
                return self.m2.group(1)
268
            if self.m3:
269
                return self.m3.group(1)
270
            return None
271
272
        def line(self):
273
            if self.m0:
274
                return int(self.m0.group(2))
275
            if self.m1:
276
                return int(self.m1.group(2))
277
            if self.m2:
278
                return int(self.m2.group(2))
279
            return 0
280
281
        def message(self):
282
            if self.m0:
283
                return self.m0.group(4)
284
            if self.m1:
285
                return self.m1.group(3)
286
            if self.m2:
287
                return self.m2.group(3)
288
            if self.m3:
289
                return self.m3.group(2)
290
            return None
291
292
        def is_included_from(self):
293
            if self.m2:
294
                return True
295
            return False
296
297
    re_message = re.compile(r'.*:\d+:\s*(\S*):\s*(.*)')
298
    re_expansion = re.compile(r_expansion)
299
    re_declaration = re.compile(r'.*declaration of\s*(.*)')
300
    re_required_from = re.compile(r'.*required from here')
301
    msg_list = []
302
    included_from_list = []
303
    msg = None
304
    prev = None
305
    is_prev_required_from = False
306
    for line in f:
307
        if options.passthrough:
308
            print(line.rstrip())
309
        if re_fatal.match(line):
310
            raise Exception(line)
311
312
        m = rmessage()
313
        if m.match(line):
314
            if msg:
315
                msg.included_from = copy.deepcopy(included_from_list)
316
                msg_list.append(msg)
317
                included_from_list[:] = []
318
                prev = msg
319
            if m.is_included_from():
320
                inc = ErrorMessage()
321
                inc.file = m.file()
322
                inc.line = m.line()
323
                inc.type = 'In file included from'
324
                included_from_list.append(inc)
325
                msg = None
326
                continue
327
            msg = ErrorMessage()
328
            msg.file = m.file()
329
            msg.line = m.line()
330
            msg.type = ""
331
            msg.root_is_expansion = root_is_expansion
332
            n = re_message.match(line)
333
            if n:
334
                msg.set_type(n.group(1))
335
                msg.message += n.group(2)
336
            else:
337
                msg.set_type('')
338
                msg.message += m.message()
339
340
            is_child = note_is_child and msg.is_note()
341
            is_type_none = prev and prev.is_type_none()
342
            is_declaration = False
343
            n = re_declaration.match(line)
344
            if n and prev and prev.message.find(n.group(1)) != -1:
345
                is_declaration = True
346
347
            if prev:
348
                is_expansion = re_expansion.search(msg.message)
349
                msg.expansion = is_expansion
350
                # print('%s - %d: %s %s %s %s' % (msg.file, msg.line, is_child, is_type_none, is_declaration, is_expansion))
351
                if is_child or is_type_none or is_declaration or is_expansion or is_prev_required_from:
352
                    prev.child = msg
353
                    msg.parent = prev
354
            is_prev_required_from = re_required_from.search(msg.message)
355
        else:
356
            if msg:
357
                msg.message += '\n'
358
                msg.message += line
359
    msg_list.append(msg)
360
    return msg_list
361
362
363
def parse_gcc(options, f):
364
    return parse_gcc_clang(options, f, r'in (definition|expansion) of macro', False, True)
365
366
367
def parse_clang(options, f):
368
    return parse_gcc_clang(options, f, r'expanded from ', True, False)
369
370
371
def parse_vc(options, f):
372
    re_fatal = re.compile(r'(\S+)\s*:\s*fatal\s*error\s*.*')
373
    re_file = re.compile(r'(\s*)(\S+)\((\d+)\)\s*:\s*(.*)')
374
    re_message = re.compile(r'.*\(\d+\)\s*: (\S*)\s*(\S*: .*)')
375
    msg_list = []
376
    msg = None
377
    prev = None
378
    for line in f:
379
        if options.passthrough:
380
            print(line.rstrip())
381
        if re_fatal.match(line):
382
            raise Exception(line)
383
384
        m = re_file.match(line)
385
        if m:
386
            if msg:
387
                msg_list.append(msg)
388
                prev = msg
389
            msg = ErrorMessage()
390
            msg.file = m.group(2)
391
            msg.line = int(m.group(3))
392
            msg.type = ""
393
            n = re_message.match(line)
394
            if n:
395
                msg.set_type(n.group(1))
396
                msg.message += n.group(2)
397
            else:
398
                msg.set_type('')
399
                msg.message += m.group(4)
400
401
            child_note = m.group(1) or (msg.is_note() and not msg.is_iutest())
402
            if (m.group(1) or child_note) and prev:
403
                prev.child = msg
404
                msg.parent = prev
405
        else:
406
            if msg:
407
                msg.message += '\n'
408
                msg.message += line
409
    msg_list.append(msg)
410
    return msg_list
411
412
413
def dump_included_from(m):
414
    for inc in m.included_from:
415
        print("%s %s:%d:" % (inc.type, inc.file, inc.line))
416
417
418
def dump_msg(m):
419
    dump_included_from(m)
420
    s = m.file
421
    s += ':'
422
    if format_gcc:
423
        if m.line:
424
            s += '%d:' % (m.line)
425
        if not m.is_type_none():
426
            s += ' %s:' % (m.type)
427
        print("%s %s" % (s, m.message))
428
    else:
429
        if m.line:
430
            s += '(%d):' % (m.line)
431
        if m.parent:
432
            print("\t%s %s %s" % (s, m.type, m.message))
433
        else:
434
            print("%s %s %s" % (s, m.type, m.message))
435
436
437
def dump_msgs(m):
438
    if m.parent:
439
        dump_msgs(m.parent)
440
    dump_msg(m)
441
442
443
def dump_list(l):
444
    for m in l:
445
        if not m.parent:
446
            print('------------------------------')
447
        dump_msg(m)
448
        print('==')
449
        if not m.child:
450
            print('------------------------------')
451
452
    return True
453
454
455
def test_result(result, msg, e):
456
    OKGREEN = '\033[32m'
457
#   WARNING = '\033[33m'
458
    FAIL = '\033[31m'
459
    ENDC = '\033[0m'
460
461
    if e:
462
        msg += ': ' + e.file + ': ' + str(e.line)
463
464
    if result:
465
        if color_prompt:
466
            print(OKGREEN + '[OK] ' + ENDC + msg)
467
        else:
468
            print('[OK] ' + msg)
469
    else:
470
        if color_prompt:
471
            print(FAIL + '[NG] ' + ENDC + msg)
472
        else:
473
            print('[NG] ' + msg)
474
475
476
def iutest(l):
477
    result = True
478
    unexpected = []
479
    re_iutest = re.compile(r'IUTEST_TEST_COMPILEERROR\( ([^#]*) \)')
480
    checkList = []
481
    messageList = []
482
    for i, msg in enumerate(l):
483
        if not msg:
484
            continue
485
        mm = re_iutest.search(msg.message)
486
        if mm:
487
            root = msg.get_source_msg()
488
            if not root.target:
489
                expect = mm.group(1).strip('"')
490
                checkList.append(ExpectMessage(root, expect, mm.group(0)))
491
                root.target = True
492
        else:
493
            messageList.append(msg)
494
495
    for msg in messageList:
496
        if msg.has_error():
497
            #print('%s - %d' % (msg.file, msg.line))
498
            for check in checkList:
499
                if msg.file in check.msg.file and msg.line == check.msg.line + 1:
500
                    actual = msg.get_error()
501
                    #print(actual.message)
502
                    msg.checked = True
503
                    if not check.expect or re.search(check.expect, actual.message):
504
                        check.msg.checked = True
505
                        break
506
            if msg.is_tail() and not msg.is_checked():
507
                unexpected.append(msg)
508
        elif msg.is_warning():
509
            dump_msg(msg)
510
511
    for check in checkList:
512
        if check.msg.checked:
513
            test_result(True, check.expression, check.msg)
514
        else:
515
            test_result(False, check.expression, check.msg)
516
            result = False
517
    for msg in unexpected:
518
        test_result(False, "Unexpected error: " + msg.message, msg)
519
        result = False
520
    return result
521
522
523
def parse_output(options):
524
    global format_gcc
525
    l = None
526
    if options.command:
527
        args = re.split('\s+', options.command)
528
        for i in range(len(args)):
529
            args[i] = os.path.expandvars(args[i])
530
        p = Popen(args, stdout=PIPE, stderr=STDOUT)
531
        p.wait()
532
        out, err = p.communicate()
533
        f = out.decode('utf-8').splitlines()
534
    else:
535
        if not options.infile:
536
            raise Exception("infile null")
537
        #print(options.infile.encoding)
538
        f = options.infile
539
        #f = codecs.getreader('utf-8')(options.infile)
540
541
    if any(options.compiler.find(s) != -1 for s in ('clang', 'clang++')):
542
        l = parse_clang(options, f)
543
    elif any(options.compiler.find(s) != -1 for s in ('gcc', 'g++')):
544
        l = parse_gcc(options, f)
545
    elif options.compiler == 'cl':
546
        format_gcc = False
547
        l = parse_vc(options, f)
548
    else:
549
        raise Exception("sorry, %s compiler is not supported", (options.compiler))
550
551
    if options.debug:
552
        dump_list(l)
553
    if options.passthrough:
554
        print('----')
555
    result = iutest(l)
556
    if not result:
557
        if options.verbose:
558
            dump_list(l)
559
    return result
560
561
def setup():
562
    global color_prompt
563
    term = os.environ.get('TERM')
564
    if term:
565
        if any(term.find(s) for s in ('xterm', 'screen', 'rxvt', 'linux', 'cygwin')):
566
            color_prompt = True
567
568
569
def main():
570
    try:
571
        options = parse_command_line()
572
        setup()
573
        if not parse_output(options):
574
            sys.exit(1)
575
    except Exception as e:
576
        print(e)
577
        raise
578
579
if __name__ == '__main__':
580
    main()
581