Compiler projects using llvm
# -*- coding: utf-8 -*-
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import json
import libear
import libscanbuild.report as sut
import unittest
import os
import os.path


def run_bug_parse(content):
    with libear.TemporaryDirectory() as tmpdir:
        file_name = os.path.join(tmpdir, 'test.html')
        with open(file_name, 'w') as handle:
            handle.writelines(content)
        for bug in sut.parse_bug_html(file_name):
            return bug


def run_crash_parse(content, preproc):
    with libear.TemporaryDirectory() as tmpdir:
        file_name = os.path.join(tmpdir, preproc + '.info.txt')
        with open(file_name, 'w') as handle:
            handle.writelines(content)
        return sut.parse_crash(file_name)


class ParseFileTest(unittest.TestCase):

    def test_parse_bug(self):
        content = [
            "some header\n",
            "<!-- BUGDESC Division by zero -->\n",
            "<!-- BUGTYPE Division by zero -->\n",
            "<!-- BUGCATEGORY Logic error -->\n",
            "<!-- BUGFILE xx -->\n",
            "<!-- BUGLINE 5 -->\n",
            "<!-- BUGCOLUMN 22 -->\n",
            "<!-- BUGPATHLENGTH 4 -->\n",
            "<!-- BUGMETAEND -->\n",
            "<!-- REPORTHEADER -->\n",
            "some tails\n"]
        result = run_bug_parse(content)
        self.assertEqual(result['bug_category'], 'Logic error')
        self.assertEqual(result['bug_path_length'], 4)
        self.assertEqual(result['bug_line'], 5)
        self.assertEqual(result['bug_description'], 'Division by zero')
        self.assertEqual(result['bug_type'], 'Division by zero')
        self.assertEqual(result['bug_file'], 'xx')

    def test_parse_bug_empty(self):
        content = []
        result = run_bug_parse(content)
        self.assertEqual(result['bug_category'], 'Other')
        self.assertEqual(result['bug_path_length'], 1)
        self.assertEqual(result['bug_line'], 0)

    def test_parse_crash(self):
        content = [
            "/some/path/file.c\n",
            "Some very serious Error\n",
            "bla\n",
            "bla-bla\n"]
        result = run_crash_parse(content, 'file.i')
        self.assertEqual(result['source'], content[0].rstrip())
        self.assertEqual(result['problem'], content[1].rstrip())
        self.assertEqual(os.path.basename(result['file']),
                         'file.i')
        self.assertEqual(os.path.basename(result['info']),
                         'file.i.info.txt')
        self.assertEqual(os.path.basename(result['stderr']),
                         'file.i.stderr.txt')

    def test_parse_real_crash(self):
        import libscanbuild.analyze as sut2
        import re
        with libear.TemporaryDirectory() as tmpdir:
            filename = os.path.join(tmpdir, 'test.c')
            with open(filename, 'w') as handle:
                handle.write('int main() { return 0')
            # produce failure report
            opts = {
                'clang': 'clang',
                'directory': os.getcwd(),
                'flags': [],
                'file': filename,
                'output_dir': tmpdir,
                'language': 'c',
                'error_type': 'other_error',
                'error_output': 'some output',
                'exit_code': 13
            }
            sut2.report_failure(opts)
            # find the info file
            pp_file = None
            for root, _, files in os.walk(tmpdir):
                keys = [os.path.join(root, name) for name in files]
                for key in keys:
                    if re.match(r'^(.*/)+clang(.*)\.i$', key):
                        pp_file = key
            self.assertIsNot(pp_file, None)
            # read the failure report back
            result = sut.parse_crash(pp_file + '.info.txt')
            self.assertEqual(result['source'], filename)
            self.assertEqual(result['problem'], 'Other Error')
            self.assertEqual(result['file'], pp_file)
            self.assertEqual(result['info'], pp_file + '.info.txt')
            self.assertEqual(result['stderr'], pp_file + '.stderr.txt')


class ReportMethodTest(unittest.TestCase):

    def test_chop(self):
        self.assertEqual('file', sut.chop('/prefix', '/prefix/file'))
        self.assertEqual('file', sut.chop('/prefix/', '/prefix/file'))
        self.assertEqual('lib/file', sut.chop('/prefix/', '/prefix/lib/file'))
        self.assertEqual('/prefix/file', sut.chop('', '/prefix/file'))

    def test_chop_when_cwd(self):
        self.assertEqual('../src/file', sut.chop('/cwd', '/src/file'))
        self.assertEqual('../src/file', sut.chop('/prefix/cwd',
                                                 '/prefix/src/file'))


class GetPrefixFromCompilationDatabaseTest(unittest.TestCase):

    def test_with_different_filenames(self):
        self.assertEqual(
            sut.commonprefix(['/tmp/a.c', '/tmp/b.c']), '/tmp')

    def test_with_different_dirnames(self):
        self.assertEqual(
            sut.commonprefix(['/tmp/abs/a.c', '/tmp/ack/b.c']), '/tmp')

    def test_no_common_prefix(self):
        self.assertEqual(
            sut.commonprefix(['/tmp/abs/a.c', '/usr/ack/b.c']), '/')

    def test_with_single_file(self):
        self.assertEqual(
            sut.commonprefix(['/tmp/a.c']), '/tmp')

    def test_empty(self):
        self.assertEqual(
            sut.commonprefix([]), '')

class MergeSarifTest(unittest.TestCase):

    def test_merging_sarif(self):
        sarif1 = {
            '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
            'runs': [
                {
                    'artifacts': [
                        {
                            'length': 100,
                            'location': {
                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
                            },
                            'mimeType': 'text/plain',
                            'roles': [
                                'resultFile'
                            ]
                        }
                    ],
                    'columnKind': 'unicodeCodePoints',
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'threadFlows': [
                                        {
                                            'locations': [
                                                {
                                                    'importance': 'important',
                                                    'location': {
                                                        'message': {
                                                            'text': 'test message 1'
                                                        },
                                                        'physicalLocation': {
                                                            'artifactLocation': {
                                                                'index': 0,
                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
                                                            },
                                                            'region': {
                                                                'endColumn': 5,
                                                                'startColumn': 1,
                                                                'startLine': 2
                                                            }
                                                        }
                                                    }
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            'codeFlows': [
                                {
                                    'threadFlows': [
                                        {
                                            'locations': [
                                                {
                                                    'importance': 'important',
                                                    'location': {
                                                        'message': {
                                                            'text': 'test message 2'
                                                        },
                                                        'physicalLocation': {
                                                            'artifactLocation': {
                                                                'index': 0,
                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
                                                            },
                                                            'region': {
                                                                'endColumn': 23,
                                                                'startColumn': 9,
                                                                'startLine': 10
                                                            }
                                                        }
                                                    }
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        }
                    ],
                    'tool': {
                        'driver': {
                            'fullName': 'clang static analyzer',
                            'language': 'en-US',
                            'name': 'clang',
                            'rules': [
                                {
                                    'fullDescription': {
                                        'text': 'test rule for merge sarif test'
                                    },
                                    'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py',
                                    'id': 'testId',
                                    'name': 'testName'
                                }
                            ],
                            'version': 'test clang'
                        }
                    }
                }
            ],
            'version': '2.1.0'
        }
        sarif2 = {
            '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
            'runs': [
                {
                    'artifacts': [
                        {
                            'length': 1523,
                            'location': {
                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
                            },
                            'mimeType': 'text/plain',
                            'roles': [
                                'resultFile'
                            ]
                        }
                    ],
                    'columnKind': 'unicodeCodePoints',
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'threadFlows': [
                                        {
                                            'locations': [
                                                {
                                                    'importance': 'important',
                                                    'location': {
                                                        'message': {
                                                            'text': 'test message 3'
                                                        },
                                                        'physicalLocation': {
                                                            'artifactLocation': {
                                                                'index': 0,
                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
                                                            },
                                                            'region': {
                                                                'endColumn': 99,
                                                                'startColumn': 99,
                                                                'startLine': 17
                                                            }
                                                        }
                                                    }
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            'codeFlows': [
                                {
                                    'threadFlows': [
                                        {
                                            'locations': [
                                                {
                                                    'importance': 'important',
                                                    'location': {
                                                        'message': {
                                                            'text': 'test message 4'
                                                        },
                                                        'physicalLocation': {
                                                            'artifactLocation': {
                                                                'index': 0,
                                                                'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
                                                            },
                                                            'region': {
                                                                'endColumn': 305,
                                                                'startColumn': 304,
                                                                'startLine': 1
                                                            }
                                                        }
                                                    }
                                                }
                                            ]
                                        }
                                    ]
                                }
                            ]
                        }
                    ],
                    'tool': {
                        'driver': {
                            'fullName': 'clang static analyzer',
                            'language': 'en-US',
                            'name': 'clang',
                            'rules': [
                                {
                                    'fullDescription': {
                                        'text': 'test rule for merge sarif test'
                                    },
                                    'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py',
                                    'id': 'testId',
                                    'name': 'testName'
                                }
                            ],
                            'version': 'test clang'
                        }
                    }
                }
            ],
            'version': '2.1.0'
        }

        contents = [sarif1, sarif2]
        with libear.TemporaryDirectory() as tmpdir:
            for idx, content in enumerate(contents):
                file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
                with open(file_name, 'w') as handle:
                    json.dump(content, handle)

            sut.merge_sarif_files(tmpdir, sort_files=True)

            self.assertIn('results-merged.sarif', os.listdir(tmpdir))
            with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
                merged = json.load(f)
                self.assertEqual(len(merged['runs']), 2)
                self.assertEqual(len(merged['runs'][0]['results']), 2)
                self.assertEqual(len(merged['runs'][1]['results']), 2)

                expected = sarif1
                for run in sarif2['runs']:
                    expected['runs'].append(run)

                self.assertEqual(merged, expected)

    def test_merge_updates_embedded_link(self):
        sarif1 = {
            'runs': [
                {
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'message': {
                                        'text': 'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
                                    },
                                    'threadFlows': [
                                        {
                                            'message': {
                                                'text': 'test message 1-2 [link](sarif:/runs/1/results/0)'
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                },
                {
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'message': {
                                        'text': 'test message 2-1 [link](sarif:/runs/0/results/0)'
                                    },
                                    'threadFlows': [
                                        {
                                            'message': {
                                                'text': 'test message 2-2 [link](sarif:/runs/0/results/0)'
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
        sarif2 = {
            'runs': [
                {
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'message': {
                                        'text': 'test message 3-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
                                    },
                                    'threadFlows': [
                                        {
                                            'message': {
                                                'text': 'test message 3-2 [link](sarif:/runs/1/results/0)'
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ],
                },
                {
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'message': {
                                        'text': 'test message 4-1 [link](sarif:/runs/0/results/0)'
                                    },
                                    'threadFlows': [
                                        {
                                            'message': {
                                                'text': 'test message 4-2 [link](sarif:/runs/0/results/0)'
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
        sarif3 = {
            'runs': [
                {
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'message': {
                                        'text': 'test message 5-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
                                    },
                                    'threadFlows': [
                                        {
                                            'message': {
                                                'text': 'test message 5-2 [link](sarif:/runs/1/results/0)'
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ],
                },
                {
                    'results': [
                        {
                            'codeFlows': [
                                {
                                    'message': {
                                        'text': 'test message 6-1 [link](sarif:/runs/0/results/0)'
                                    },
                                    'threadFlows': [
                                        {
                                            'message': {
                                                'text': 'test message 6-2 [link](sarif:/runs/0/results/0)'
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }

        contents = [sarif1, sarif2, sarif3]

        with libear.TemporaryDirectory() as tmpdir:
            for idx, content in enumerate(contents):
                file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
                with open(file_name, 'w') as handle:
                    json.dump(content, handle)

            sut.merge_sarif_files(tmpdir, sort_files=True)

            self.assertIn('results-merged.sarif', os.listdir(tmpdir))
            with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
                merged = json.load(f)
                self.assertEqual(len(merged['runs']), 6)

                code_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['message']['text'] for x in range(6)]
                thread_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['threadFlows'][0]['message']['text'] for x in range(6)]

                # The run index should be updated for the second and third sets of runs
                self.assertEqual(code_flows,
                    [
                        'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)',
                        'test message 2-1 [link](sarif:/runs/0/results/0)',
                        'test message 3-1 [link](sarif:/runs/3/results/0) [link2](sarif:/runs/3/results/0)',
                        'test message 4-1 [link](sarif:/runs/2/results/0)',
                        'test message 5-1 [link](sarif:/runs/5/results/0) [link2](sarif:/runs/5/results/0)',
                        'test message 6-1 [link](sarif:/runs/4/results/0)'
                    ])
                self.assertEquals(thread_flows,
                    [
                        'test message 1-2 [link](sarif:/runs/1/results/0)',
                        'test message 2-2 [link](sarif:/runs/0/results/0)',
                        'test message 3-2 [link](sarif:/runs/3/results/0)',
                        'test message 4-2 [link](sarif:/runs/2/results/0)',
                        'test message 5-2 [link](sarif:/runs/5/results/0)',
                        'test message 6-2 [link](sarif:/runs/4/results/0)'
                    ])

    def test_overflow_run_count(self):
        sarif1 = {
            'runs': [
                {'results': [{
                    'message': {'text': 'run 1-0 [link](sarif:/runs/1/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-1 [link](sarif:/runs/2/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-2 [link](sarif:/runs/3/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-3 [link](sarif:/runs/4/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-4 [link](sarif:/runs/5/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-5 [link](sarif:/runs/6/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-6 [link](sarif:/runs/7/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-7 [link](sarif:/runs/8/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-8 [link](sarif:/runs/9/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 1-9 [link](sarif:/runs/0/results/0)'}
                }]}
            ]
        }
        sarif2 = {
            'runs': [
                {'results': [{
                    'message': {'text': 'run 2-0 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/2/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-1 [link](sarif:/runs/2/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-2 [link](sarif:/runs/3/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-3 [link](sarif:/runs/4/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-4 [link](sarif:/runs/5/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-5 [link](sarif:/runs/6/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-6 [link](sarif:/runs/7/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-7 [link](sarif:/runs/8/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-8 [link](sarif:/runs/9/results/0)'}
                }]},
                {'results': [{
                    'message': {'text': 'run 2-9 [link](sarif:/runs/0/results/0)'}
                }]}
            ]
        }

        contents = [sarif1, sarif2]
        with libear.TemporaryDirectory() as tmpdir:
            for idx, content in enumerate(contents):
                file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
                with open(file_name, 'w') as handle:
                    json.dump(content, handle)

            sut.merge_sarif_files(tmpdir, sort_files=True)

            self.assertIn('results-merged.sarif', os.listdir(tmpdir))
            with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
                merged = json.load(f)
                self.assertEqual(len(merged['runs']), 20)

                messages = [merged['runs'][x]['results'][0]['message']['text'] for x in range(20)]
                self.assertEqual(messages,
                    [
                        'run 1-0 [link](sarif:/runs/1/results/0)',
                        'run 1-1 [link](sarif:/runs/2/results/0)',
                        'run 1-2 [link](sarif:/runs/3/results/0)',
                        'run 1-3 [link](sarif:/runs/4/results/0)',
                        'run 1-4 [link](sarif:/runs/5/results/0)',
                        'run 1-5 [link](sarif:/runs/6/results/0)',
                        'run 1-6 [link](sarif:/runs/7/results/0)',
                        'run 1-7 [link](sarif:/runs/8/results/0)',
                        'run 1-8 [link](sarif:/runs/9/results/0)',
                        'run 1-9 [link](sarif:/runs/0/results/0)',
                        'run 2-0 [link](sarif:/runs/11/results/0) [link2](sarif:/runs/12/results/0)',
                        'run 2-1 [link](sarif:/runs/12/results/0)',
                        'run 2-2 [link](sarif:/runs/13/results/0)',
                        'run 2-3 [link](sarif:/runs/14/results/0)',
                        'run 2-4 [link](sarif:/runs/15/results/0)',
                        'run 2-5 [link](sarif:/runs/16/results/0)',
                        'run 2-6 [link](sarif:/runs/17/results/0)',
                        'run 2-7 [link](sarif:/runs/18/results/0)',
                        'run 2-8 [link](sarif:/runs/19/results/0)',
                        'run 2-9 [link](sarif:/runs/10/results/0)'
                    ])