#!/usr/bin/env python

# Copyright 1996-2021 Cyberbotics Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Test that the C, C++ and shader source code is compliant with ClangFormat."""
import unittest

import difflib
import os
import subprocess

from io import open

from distutils.spawn import find_executable


class TestClangFormat(unittest.TestCase):
    """Unit test for ClangFormat compliance."""

    def setUp(self):
        """Set up called before each test."""
        self.WEBOTS_HOME = os.environ['WEBOTS_HOME']

    def _runClangFormat(self, f):
        """Run clang format on 'f' file."""
        return subprocess.check_output(['clang-format', '-style=file', f])

    def test_clang_format_is_correctly_installed(self):
        """Test ClangFormat is correctly installed."""
        self.assertTrue(
            find_executable('clang-format') is not None,
            msg='ClangFormat is not installed on this computer.'
        )
        clangFormatConfigFile = self.WEBOTS_HOME + os.sep + '.clang-format'
        self.assertTrue(
            os.path.exists(clangFormatConfigFile),
            msg=clangFormatConfigFile + ' not found.'
        )

    def test_sources_are_clang_format_compliant(self):
        """Test that sources are ClangFormat compliant."""
        directories = [
            'include/controller',
            'projects',
            'resources/projects',
            'resources/wren/shaders',
            'tests',
            'include/wren',
            'src/controller/c',
            'src/controller/cpp',
            'src/license/sign',
            'src/webots',
            'src/wren'
        ]
        skippedPaths = [
            'projects/default/controllers/ros/include',
            'projects/robots/gctronic/e-puck/transfer',
            'projects/robots/mobsya/thymio/controllers/thymio2_aseba/aseba',
            'projects/robots/mobsya/thymio/libraries/dashel',
            'projects/robots/mobsya/thymio/libraries/dashel-src',
            'projects/robots/robotis/darwin-op/libraries/libssh',
            'projects/robots/robotis/darwin-op/libraries/libzip',
            'projects/robots/robotis/darwin-op/libraries/robotis-op2/robotis',
            'projects/robots/robotis/darwin-op/remote_control/libjpeg-turbo',
            'projects/vehicles/controllers/ros_automobile/include',
            'src/webots/external'
        ]
        skippedFiles = [
            'projects/robots/robotis/darwin-op/plugins/remote_controls/robotis-op2_tcpip/stb_image.h'
        ]
        skippedDirectories = [
            'build',
            'python',
            'java'
        ]
        extensions = ['c', 'h', 'cpp', 'hpp', 'cc', 'hh', 'c++', 'h++', 'vert', 'frag']
        modified_files = os.path.join(self.WEBOTS_HOME, 'tests', 'sources', 'modified_files.txt')
        sources = []
        if os.path.isfile(modified_files):
            with open(modified_files, 'r') as file:
                for line in file:
                    line = line.strip()
                    extension = os.path.splitext(line)[1][1:].lower()
                    if extension not in extensions:
                        continue
                    found = False
                    for directory in directories:
                        if line.startswith(directory):
                            found = True
                            break
                    if not found:
                        continue
                    found = False
                    for directory in skippedPaths + skippedFiles:
                        if line.startswith(directory):
                            found = True
                            break
                    if found:
                        continue
                    for directory in skippedDirectories:
                        currentDirectories = line.split(os.sep)
                        if directory in currentDirectories:
                            found = True
                    if found:
                        continue
                    sources.append(line.replace('/', os.sep))
        else:
            for directory in directories:
                path = self.WEBOTS_HOME + os.sep + directory.replace('/', os.sep)
                for rootPath, dirNames, fileNames in os.walk(path):
                    shouldContinue = False
                    for path in skippedPaths:
                        if rootPath.startswith(self.WEBOTS_HOME + os.sep + path.replace('/', os.sep)):
                            shouldContinue = True
                            break
                    for directory in skippedDirectories:
                        currentDirectories = rootPath.replace(self.WEBOTS_HOME, '').split(os.sep)
                        if directory in currentDirectories:
                            shouldContinue = True
                            break
                    if shouldContinue:
                        continue
                    for fileName in fileNames:
                        extension = os.path.splitext(fileName)[1][1:].lower()
                        if extension not in extensions:
                            continue
                        path = os.path.normpath(os.path.join(rootPath, fileName))
                        skipFile = False
                        for file in skippedFiles:
                            if os.path.normpath((self.WEBOTS_HOME + os.sep + file.replace('/', os.sep))) == path:
                                skipFile = True
                                break
                        if not skipFile:
                            sources.append(path)
        curdir = os.getcwd()
        os.chdir(self.WEBOTS_HOME)
        for source in sources:
            diff = ''
            with open(source, encoding='utf8') as file:
                try:
                    for line in difflib.context_diff(self._runClangFormat(source).decode('utf-8').splitlines(),
                                                     file.read().splitlines()):
                        diff += line + '\n'
                except UnicodeDecodeError:
                    self.assertTrue(False, msg='utf-8 decode problem in %s' % source)
                self.assertTrue(
                    len(diff) == 0,
                    msg='Source file "%s" is not compliant with ClangFormat:\n\nDIFF:%s' % (source, diff)
                )
        os.chdir(curdir)


if __name__ == '__main__':
    unittest.main()

[((166, 4, 166, 19), 'unittest.main', 'unittest.main', ({}, {}), '()', False, 'import unittest\n'), ((38, 15, 38, 74), 'subprocess.check_output', 'subprocess.check_output', ({(38, 39, 38, 73): "['clang-format', '-style=file', f]"}, {}), "(['clang-format', '-style=file', f])", False, 'import subprocess\n'), ((89, 25, 89, 97), 'os.path.join', 'os.path.join', ({(89, 38, 89, 54): 'self.WEBOTS_HOME', (89, 56, 89, 63): '"""tests"""', (89, 65, 89, 74): '"""sources"""', (89, 76, 89, 96): '"""modified_files.txt"""'}, {}), "(self.WEBOTS_HOME, 'tests', 'sources', 'modified_files.txt')", False, 'import os\n'), ((91, 11, 91, 41), 'os.path.isfile', 'os.path.isfile', ({(91, 26, 91, 40): 'modified_files'}, {}), '(modified_files)', False, 'import os\n'), ((147, 17, 147, 28), 'os.getcwd', 'os.getcwd', ({}, {}), '()', False, 'import os\n'), ((148, 8, 148, 34), 'os.chdir', 'os.chdir', ({(148, 17, 148, 33): 'self.WEBOTS_HOME'}, {}), '(self.WEBOTS_HOME)', False, 'import os\n'), ((162, 8, 162, 24), 'os.chdir', 'os.chdir', ({(162, 17, 162, 23): 'curdir'}, {}), '(curdir)', False, 'import os\n'), ((48, 12, 48, 49), 'os.path.exists', 'os.path.exists', ({(48, 27, 48, 48): 'clangFormatConfigFile'}, {}), '(clangFormatConfigFile)', False, 'import os\n'), ((43, 12, 43, 43), 'distutils.spawn.find_executable', 'find_executable', ({(43, 28, 43, 42): '"""clang-format"""'}, {}), "('clang-format')", False, 'from distutils.spawn import find_executable\n'), ((92, 17, 92, 42), 'io.open', 'open', ({(92, 22, 92, 36): 'modified_files', (92, 38, 92, 41): '"""r"""'}, {}), "(modified_files, 'r')", False, 'from io import open\n'), ((122, 53, 122, 66), 'os.walk', 'os.walk', ({(122, 61, 122, 65): 'path'}, {}), '(path)', False, 'import os\n'), ((151, 17, 151, 46), 'io.open', 'open', (), '', False, 'from io import open\n'), ((139, 48, 139, 80), 'os.path.join', 'os.path.join', ({(139, 61, 139, 69): 'rootPath', (139, 71, 139, 79): 'fileName'}, {}), '(rootPath, fileName)', False, 'import os\n'), ((95, 32, 95, 54), 'os.path.splitext', 'os.path.splitext', ({(95, 49, 95, 53): 'line'}, {}), '(line)', False, 'import os\n'), ((136, 36, 136, 62), 'os.path.splitext', 'os.path.splitext', ({(136, 53, 136, 61): 'fileName'}, {}), '(fileName)', False, 'import os\n')]