1# -*- coding: utf-8 -*-
2# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
3# See https://llvm.org/LICENSE.txt for license information.
4# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5
6import json
7import libear
8import libscanbuild.report as sut
9import unittest
10import os
11import os.path
12
13
14def run_bug_parse(content):
15    with libear.TemporaryDirectory() as tmpdir:
16        file_name = os.path.join(tmpdir, 'test.html')
17        with open(file_name, 'w') as handle:
18            handle.writelines(content)
19        for bug in sut.parse_bug_html(file_name):
20            return bug
21
22
23def run_crash_parse(content, preproc):
24    with libear.TemporaryDirectory() as tmpdir:
25        file_name = os.path.join(tmpdir, preproc + '.info.txt')
26        with open(file_name, 'w') as handle:
27            handle.writelines(content)
28        return sut.parse_crash(file_name)
29
30
31class ParseFileTest(unittest.TestCase):
32
33    def test_parse_bug(self):
34        content = [
35            "some header\n",
36            "<!-- BUGDESC Division by zero -->\n",
37            "<!-- BUGTYPE Division by zero -->\n",
38            "<!-- BUGCATEGORY Logic error -->\n",
39            "<!-- BUGFILE xx -->\n",
40            "<!-- BUGLINE 5 -->\n",
41            "<!-- BUGCOLUMN 22 -->\n",
42            "<!-- BUGPATHLENGTH 4 -->\n",
43            "<!-- BUGMETAEND -->\n",
44            "<!-- REPORTHEADER -->\n",
45            "some tails\n"]
46        result = run_bug_parse(content)
47        self.assertEqual(result['bug_category'], 'Logic error')
48        self.assertEqual(result['bug_path_length'], 4)
49        self.assertEqual(result['bug_line'], 5)
50        self.assertEqual(result['bug_description'], 'Division by zero')
51        self.assertEqual(result['bug_type'], 'Division by zero')
52        self.assertEqual(result['bug_file'], 'xx')
53
54    def test_parse_bug_empty(self):
55        content = []
56        result = run_bug_parse(content)
57        self.assertEqual(result['bug_category'], 'Other')
58        self.assertEqual(result['bug_path_length'], 1)
59        self.assertEqual(result['bug_line'], 0)
60
61    def test_parse_crash(self):
62        content = [
63            "/some/path/file.c\n",
64            "Some very serious Error\n",
65            "bla\n",
66            "bla-bla\n"]
67        result = run_crash_parse(content, 'file.i')
68        self.assertEqual(result['source'], content[0].rstrip())
69        self.assertEqual(result['problem'], content[1].rstrip())
70        self.assertEqual(os.path.basename(result['file']),
71                         'file.i')
72        self.assertEqual(os.path.basename(result['info']),
73                         'file.i.info.txt')
74        self.assertEqual(os.path.basename(result['stderr']),
75                         'file.i.stderr.txt')
76
77    def test_parse_real_crash(self):
78        import libscanbuild.analyze as sut2
79        import re
80        with libear.TemporaryDirectory() as tmpdir:
81            filename = os.path.join(tmpdir, 'test.c')
82            with open(filename, 'w') as handle:
83                handle.write('int main() { return 0')
84            # produce failure report
85            opts = {
86                'clang': 'clang',
87                'directory': os.getcwd(),
88                'flags': [],
89                'file': filename,
90                'output_dir': tmpdir,
91                'language': 'c',
92                'error_type': 'other_error',
93                'error_output': 'some output',
94                'exit_code': 13
95            }
96            sut2.report_failure(opts)
97            # find the info file
98            pp_file = None
99            for root, _, files in os.walk(tmpdir):
100                keys = [os.path.join(root, name) for name in files]
101                for key in keys:
102                    if re.match(r'^(.*/)+clang(.*)\.i$', key):
103                        pp_file = key
104            self.assertIsNot(pp_file, None)
105            # read the failure report back
106            result = sut.parse_crash(pp_file + '.info.txt')
107            self.assertEqual(result['source'], filename)
108            self.assertEqual(result['problem'], 'Other Error')
109            self.assertEqual(result['file'], pp_file)
110            self.assertEqual(result['info'], pp_file + '.info.txt')
111            self.assertEqual(result['stderr'], pp_file + '.stderr.txt')
112
113
114class ReportMethodTest(unittest.TestCase):
115
116    def test_chop(self):
117        self.assertEqual('file', sut.chop('/prefix', '/prefix/file'))
118        self.assertEqual('file', sut.chop('/prefix/', '/prefix/file'))
119        self.assertEqual('lib/file', sut.chop('/prefix/', '/prefix/lib/file'))
120        self.assertEqual('/prefix/file', sut.chop('', '/prefix/file'))
121
122    def test_chop_when_cwd(self):
123        self.assertEqual('../src/file', sut.chop('/cwd', '/src/file'))
124        self.assertEqual('../src/file', sut.chop('/prefix/cwd',
125                                                 '/prefix/src/file'))
126
127
128class GetPrefixFromCompilationDatabaseTest(unittest.TestCase):
129
130    def test_with_different_filenames(self):
131        self.assertEqual(
132            sut.commonprefix(['/tmp/a.c', '/tmp/b.c']), '/tmp')
133
134    def test_with_different_dirnames(self):
135        self.assertEqual(
136            sut.commonprefix(['/tmp/abs/a.c', '/tmp/ack/b.c']), '/tmp')
137
138    def test_no_common_prefix(self):
139        self.assertEqual(
140            sut.commonprefix(['/tmp/abs/a.c', '/usr/ack/b.c']), '/')
141
142    def test_with_single_file(self):
143        self.assertEqual(
144            sut.commonprefix(['/tmp/a.c']), '/tmp')
145
146    def test_empty(self):
147        self.assertEqual(
148            sut.commonprefix([]), '')
149
150class MergeSarifTest(unittest.TestCase):
151
152    def test_merging_sarif(self):
153        sarif1 = {
154            '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
155            'runs': [
156                {
157                    'artifacts': [
158                        {
159                            'length': 100,
160                            'location': {
161                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
162                            },
163                            'mimeType': 'text/plain',
164                            'roles': [
165                                'resultFile'
166                            ]
167                        }
168                    ],
169                    'columnKind': 'unicodeCodePoints',
170                    'results': [
171                        {
172                            'codeFlows': [
173                                {
174                                    'threadFlows': [
175                                        {
176                                            'locations': [
177                                                {
178                                                    'importance': 'important',
179                                                    'location': {
180                                                        'message': {
181                                                            'text': 'test message 1'
182                                                        },
183                                                        'physicalLocation': {
184                                                            'artifactLocation': {
185                                                                'index': 0,
186                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
187                                                            },
188                                                            'region': {
189                                                                'endColumn': 5,
190                                                                'startColumn': 1,
191                                                                'startLine': 2
192                                                            }
193                                                        }
194                                                    }
195                                                }
196                                            ]
197                                        }
198                                    ]
199                                }
200                            ]
201                        },
202                        {
203                            'codeFlows': [
204                                {
205                                    'threadFlows': [
206                                        {
207                                            'locations': [
208                                                {
209                                                    'importance': 'important',
210                                                    'location': {
211                                                        'message': {
212                                                            'text': 'test message 2'
213                                                        },
214                                                        'physicalLocation': {
215                                                            'artifactLocation': {
216                                                                'index': 0,
217                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
218                                                            },
219                                                            'region': {
220                                                                'endColumn': 23,
221                                                                'startColumn': 9,
222                                                                'startLine': 10
223                                                            }
224                                                        }
225                                                    }
226                                                }
227                                            ]
228                                        }
229                                    ]
230                                }
231                            ]
232                        }
233                    ],
234                    'tool': {
235                        'driver': {
236                            'fullName': 'clang static analyzer',
237                            'language': 'en-US',
238                            'name': 'clang',
239                            'rules': [
240                                {
241                                    'fullDescription': {
242                                        'text': 'test rule for merge sarif test'
243                                    },
244                                    'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py',
245                                    'id': 'testId',
246                                    'name': 'testName'
247                                }
248                            ],
249                            'version': 'test clang'
250                        }
251                    }
252                }
253            ],
254            'version': '2.1.0'
255        }
256        sarif2 = {
257            '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
258            'runs': [
259                {
260                    'artifacts': [
261                        {
262                            'length': 1523,
263                            'location': {
264                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
265                            },
266                            'mimeType': 'text/plain',
267                            'roles': [
268                                'resultFile'
269                            ]
270                        }
271                    ],
272                    'columnKind': 'unicodeCodePoints',
273                    'results': [
274                        {
275                            'codeFlows': [
276                                {
277                                    'threadFlows': [
278                                        {
279                                            'locations': [
280                                                {
281                                                    'importance': 'important',
282                                                    'location': {
283                                                        'message': {
284                                                            'text': 'test message 3'
285                                                        },
286                                                        'physicalLocation': {
287                                                            'artifactLocation': {
288                                                                'index': 0,
289                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
290                                                            },
291                                                            'region': {
292                                                                'endColumn': 99,
293                                                                'startColumn': 99,
294                                                                'startLine': 17
295                                                            }
296                                                        }
297                                                    }
298                                                }
299                                            ]
300                                        }
301                                    ]
302                                }
303                            ]
304                        },
305                        {
306                            'codeFlows': [
307                                {
308                                    'threadFlows': [
309                                        {
310                                            'locations': [
311                                                {
312                                                    'importance': 'important',
313                                                    'location': {
314                                                        'message': {
315                                                            'text': 'test message 4'
316                                                        },
317                                                        'physicalLocation': {
318                                                            'artifactLocation': {
319                                                                'index': 0,
320                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
321                                                            },
322                                                            'region': {
323                                                                'endColumn': 305,
324                                                                'startColumn': 304,
325                                                                'startLine': 1
326                                                            }
327                                                        }
328                                                    }
329                                                }
330                                            ]
331                                        }
332                                    ]
333                                }
334                            ]
335                        }
336                    ],
337                    'tool': {
338                        'driver': {
339                            'fullName': 'clang static analyzer',
340                            'language': 'en-US',
341                            'name': 'clang',
342                            'rules': [
343                                {
344                                    'fullDescription': {
345                                        'text': 'test rule for merge sarif test'
346                                    },
347                                    'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py',
348                                    'id': 'testId',
349                                    'name': 'testName'
350                                }
351                            ],
352                            'version': 'test clang'
353                        }
354                    }
355                }
356            ],
357            'version': '2.1.0'
358        }
359
360        contents = [sarif1, sarif2]
361        with libear.TemporaryDirectory() as tmpdir:
362            for idx, content in enumerate(contents):
363                file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
364                with open(file_name, 'w') as handle:
365                    json.dump(content, handle)
366
367            sut.merge_sarif_files(tmpdir, sort_files=True)
368
369            self.assertIn('results-merged.sarif', os.listdir(tmpdir))
370            with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
371                merged = json.load(f)
372                self.assertEqual(len(merged['runs']), 2)
373                self.assertEqual(len(merged['runs'][0]['results']), 2)
374                self.assertEqual(len(merged['runs'][1]['results']), 2)
375
376                expected = sarif1
377                for run in sarif2['runs']:
378                    expected['runs'].append(run)
379
380                self.assertEqual(merged, expected)
381
382    def test_merge_updates_embedded_link(self):
383        sarif1 = {
384            'runs': [
385                {
386                    'results': [
387                        {
388                            'codeFlows': [
389                                {
390                                    'message': {
391                                        'text': 'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
392                                    },
393                                    'threadFlows': [
394                                        {
395                                            'message': {
396                                                'text': 'test message 1-2 [link](sarif:/runs/1/results/0)'
397                                            }
398                                        }
399                                    ]
400                                }
401                            ]
402                        }
403                    ]
404                },
405                {
406                    'results': [
407                        {
408                            'codeFlows': [
409                                {
410                                    'message': {
411                                        'text': 'test message 2-1 [link](sarif:/runs/0/results/0)'
412                                    },
413                                    'threadFlows': [
414                                        {
415                                            'message': {
416                                                'text': 'test message 2-2 [link](sarif:/runs/0/results/0)'
417                                            }
418                                        }
419                                    ]
420                                }
421                            ]
422                        }
423                    ]
424                }
425            ]
426        }
427        sarif2 = {
428            'runs': [
429                {
430                    'results': [
431                        {
432                            'codeFlows': [
433                                {
434                                    'message': {
435                                        'text': 'test message 3-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
436                                    },
437                                    'threadFlows': [
438                                        {
439                                            'message': {
440                                                'text': 'test message 3-2 [link](sarif:/runs/1/results/0)'
441                                            }
442                                        }
443                                    ]
444                                }
445                            ]
446                        }
447                    ],
448                },
449                {
450                    'results': [
451                        {
452                            'codeFlows': [
453                                {
454                                    'message': {
455                                        'text': 'test message 4-1 [link](sarif:/runs/0/results/0)'
456                                    },
457                                    'threadFlows': [
458                                        {
459                                            'message': {
460                                                'text': 'test message 4-2 [link](sarif:/runs/0/results/0)'
461                                            }
462                                        }
463                                    ]
464                                }
465                            ]
466                        }
467                    ]
468                }
469            ]
470        }
471        sarif3 = {
472            'runs': [
473                {
474                    'results': [
475                        {
476                            'codeFlows': [
477                                {
478                                    'message': {
479                                        'text': 'test message 5-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
480                                    },
481                                    'threadFlows': [
482                                        {
483                                            'message': {
484                                                'text': 'test message 5-2 [link](sarif:/runs/1/results/0)'
485                                            }
486                                        }
487                                    ]
488                                }
489                            ]
490                        }
491                    ],
492                },
493                {
494                    'results': [
495                        {
496                            'codeFlows': [
497                                {
498                                    'message': {
499                                        'text': 'test message 6-1 [link](sarif:/runs/0/results/0)'
500                                    },
501                                    'threadFlows': [
502                                        {
503                                            'message': {
504                                                'text': 'test message 6-2 [link](sarif:/runs/0/results/0)'
505                                            }
506                                        }
507                                    ]
508                                }
509                            ]
510                        }
511                    ]
512                }
513            ]
514        }
515
516        contents = [sarif1, sarif2, sarif3]
517
518        with libear.TemporaryDirectory() as tmpdir:
519            for idx, content in enumerate(contents):
520                file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
521                with open(file_name, 'w') as handle:
522                    json.dump(content, handle)
523
524            sut.merge_sarif_files(tmpdir, sort_files=True)
525
526            self.assertIn('results-merged.sarif', os.listdir(tmpdir))
527            with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
528                merged = json.load(f)
529                self.assertEqual(len(merged['runs']), 6)
530
531                code_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['message']['text'] for x in range(6)]
532                thread_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['threadFlows'][0]['message']['text'] for x in range(6)]
533
534                # The run index should be updated for the second and third sets of runs
535                self.assertEqual(code_flows,
536                    [
537                        'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)',
538                        'test message 2-1 [link](sarif:/runs/0/results/0)',
539                        'test message 3-1 [link](sarif:/runs/3/results/0) [link2](sarif:/runs/3/results/0)',
540                        'test message 4-1 [link](sarif:/runs/2/results/0)',
541                        'test message 5-1 [link](sarif:/runs/5/results/0) [link2](sarif:/runs/5/results/0)',
542                        'test message 6-1 [link](sarif:/runs/4/results/0)'
543                    ])
544                self.assertEquals(thread_flows,
545                    [
546                        'test message 1-2 [link](sarif:/runs/1/results/0)',
547                        'test message 2-2 [link](sarif:/runs/0/results/0)',
548                        'test message 3-2 [link](sarif:/runs/3/results/0)',
549                        'test message 4-2 [link](sarif:/runs/2/results/0)',
550                        'test message 5-2 [link](sarif:/runs/5/results/0)',
551                        'test message 6-2 [link](sarif:/runs/4/results/0)'
552                    ])
553
554    def test_overflow_run_count(self):
555        sarif1 = {
556            'runs': [
557                {'results': [{
558                    'message': {'text': 'run 1-0 [link](sarif:/runs/1/results/0)'}
559                }]},
560                {'results': [{
561                    'message': {'text': 'run 1-1 [link](sarif:/runs/2/results/0)'}
562                }]},
563                {'results': [{
564                    'message': {'text': 'run 1-2 [link](sarif:/runs/3/results/0)'}
565                }]},
566                {'results': [{
567                    'message': {'text': 'run 1-3 [link](sarif:/runs/4/results/0)'}
568                }]},
569                {'results': [{
570                    'message': {'text': 'run 1-4 [link](sarif:/runs/5/results/0)'}
571                }]},
572                {'results': [{
573                    'message': {'text': 'run 1-5 [link](sarif:/runs/6/results/0)'}
574                }]},
575                {'results': [{
576                    'message': {'text': 'run 1-6 [link](sarif:/runs/7/results/0)'}
577                }]},
578                {'results': [{
579                    'message': {'text': 'run 1-7 [link](sarif:/runs/8/results/0)'}
580                }]},
581                {'results': [{
582                    'message': {'text': 'run 1-8 [link](sarif:/runs/9/results/0)'}
583                }]},
584                {'results': [{
585                    'message': {'text': 'run 1-9 [link](sarif:/runs/0/results/0)'}
586                }]}
587            ]
588        }
589        sarif2 = {
590            'runs': [
591                {'results': [{
592                    'message': {'text': 'run 2-0 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/2/results/0)'}
593                }]},
594                {'results': [{
595                    'message': {'text': 'run 2-1 [link](sarif:/runs/2/results/0)'}
596                }]},
597                {'results': [{
598                    'message': {'text': 'run 2-2 [link](sarif:/runs/3/results/0)'}
599                }]},
600                {'results': [{
601                    'message': {'text': 'run 2-3 [link](sarif:/runs/4/results/0)'}
602                }]},
603                {'results': [{
604                    'message': {'text': 'run 2-4 [link](sarif:/runs/5/results/0)'}
605                }]},
606                {'results': [{
607                    'message': {'text': 'run 2-5 [link](sarif:/runs/6/results/0)'}
608                }]},
609                {'results': [{
610                    'message': {'text': 'run 2-6 [link](sarif:/runs/7/results/0)'}
611                }]},
612                {'results': [{
613                    'message': {'text': 'run 2-7 [link](sarif:/runs/8/results/0)'}
614                }]},
615                {'results': [{
616                    'message': {'text': 'run 2-8 [link](sarif:/runs/9/results/0)'}
617                }]},
618                {'results': [{
619                    'message': {'text': 'run 2-9 [link](sarif:/runs/0/results/0)'}
620                }]}
621            ]
622        }
623
624        contents = [sarif1, sarif2]
625        with libear.TemporaryDirectory() as tmpdir:
626            for idx, content in enumerate(contents):
627                file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
628                with open(file_name, 'w') as handle:
629                    json.dump(content, handle)
630
631            sut.merge_sarif_files(tmpdir, sort_files=True)
632
633            self.assertIn('results-merged.sarif', os.listdir(tmpdir))
634            with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
635                merged = json.load(f)
636                self.assertEqual(len(merged['runs']), 20)
637
638                messages = [merged['runs'][x]['results'][0]['message']['text'] for x in range(20)]
639                self.assertEqual(messages,
640                    [
641                        'run 1-0 [link](sarif:/runs/1/results/0)',
642                        'run 1-1 [link](sarif:/runs/2/results/0)',
643                        'run 1-2 [link](sarif:/runs/3/results/0)',
644                        'run 1-3 [link](sarif:/runs/4/results/0)',
645                        'run 1-4 [link](sarif:/runs/5/results/0)',
646                        'run 1-5 [link](sarif:/runs/6/results/0)',
647                        'run 1-6 [link](sarif:/runs/7/results/0)',
648                        'run 1-7 [link](sarif:/runs/8/results/0)',
649                        'run 1-8 [link](sarif:/runs/9/results/0)',
650                        'run 1-9 [link](sarif:/runs/0/results/0)',
651                        'run 2-0 [link](sarif:/runs/11/results/0) [link2](sarif:/runs/12/results/0)',
652                        'run 2-1 [link](sarif:/runs/12/results/0)',
653                        'run 2-2 [link](sarif:/runs/13/results/0)',
654                        'run 2-3 [link](sarif:/runs/14/results/0)',
655                        'run 2-4 [link](sarif:/runs/15/results/0)',
656                        'run 2-5 [link](sarif:/runs/16/results/0)',
657                        'run 2-6 [link](sarif:/runs/17/results/0)',
658                        'run 2-7 [link](sarif:/runs/18/results/0)',
659                        'run 2-8 [link](sarif:/runs/19/results/0)',
660                        'run 2-9 [link](sarif:/runs/10/results/0)'
661                    ])
662