#!/usr/bin/env python

##===--- iwyu_tool_test.py - test for iwyu_tool.py ------------------------===##
#
#                     The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
##===----------------------------------------------------------------------===##
import os
import sys
import time
import random
import inspect
import unittest
import iwyu_tool

try:
    from cStringIO import StringIO
except ImportError:
    from io import StringIO


class MockProcess(object):
    def __init__(self, block, content, returncode):
        self.content = content
        self.complete_ts = time.time() + block
        self.returncode = returncode

    def poll(self):
        if time.time() < self.complete_ts:
            return None
        return 0

    def get_output(self):
        remaining = self.complete_ts - time.time()
        if remaining > 0:
            time.sleep(remaining)
        return self.content


class MockInvocation(iwyu_tool.Invocation):
    def __init__(self, command=None, cwd=''):
        iwyu_tool.Invocation.__init__(self, command or [], cwd)
        self._will_return = ''
        self._will_block = 0
        self._will_returncode = 0

    def will_block(self, seconds):
        self._will_block = seconds

    def will_return(self, content):
        self._will_return = content

    def will_returncode(self, returncode):
        self._will_returncode = returncode

    def start(self, verbose):
        return MockProcess(self._will_block, self._will_return,
                           self._will_returncode)


class MockIwyuToolMain(object):
    """ Replacement for iwyu_tool.main to capture parsed arguments. """
    def __init__(self):
        if hasattr(inspect, 'getfullargspec'):
            getargspec = inspect.getfullargspec
        else:
            getargspec = inspect.getargspec
        self.argspec = getargspec(iwyu_tool.main).args
        self.real_iwyu_tool_main = iwyu_tool.main
        iwyu_tool.main = self._mock
        self.call_args = {}

    def reset(self):
        iwyu_tool.main = self.real_iwyu_tool_main

    def _mock(self, *args, **kwargs):
        for i, arg in enumerate(args):
            name = self.argspec[i]
            self.call_args[name] = arg

        self.call_args.update(kwargs)
        return 0


class IWYUToolTests(unittest.TestCase):
    def _execute(self, invocations, verbose=False, formatter=None, jobs=1):
        formatter = formatter or iwyu_tool.DEFAULT_FORMAT
        formatter = iwyu_tool.FORMATTERS.get(formatter, formatter)
        return iwyu_tool.execute(invocations, verbose, formatter, jobs)

    def setUp(self):
        self.stdout_stub = StringIO()
        iwyu_tool.sys.stdout = self.stdout_stub

    def test_from_compile_command(self):
        extra_args = ['-foo']
        invocation = iwyu_tool.Invocation.from_compile_command(
            {
                'directory': '/home/user/llvm/build',
                'command': '/usr/bin/clang++ -Iinclude file.cc',
                'file': 'file.cc'
            }, extra_args)
        self.assertEqual(
            invocation.command,
            [iwyu_tool.IWYU_EXECUTABLE, '-foo', '-Iinclude', 'file.cc'])
        self.assertEqual(invocation.cwd, '/home/user/llvm/build')

    def test_invocation(self):
        invocation = MockInvocation()
        invocation.will_return('BAR')
        self._execute([invocation])
        self.assertEqual(self.stdout_stub.getvalue(), 'BAR\n')

    def test_order_asynchronous(self):
        invocations = [MockInvocation() for _ in range(100)]
        for n, invocation in enumerate(invocations):
            invocation.will_return('BAR%d' % n)
            invocation.will_block(random.random() / 100)
        self._execute(invocations, jobs=100)
        self.assertSetEqual(
            set('BAR%d' % n for n in range(100)),
            set(self.stdout_stub.getvalue().splitlines()))

    def test_order_synchronous(self):
        invocations = [MockInvocation() for _ in range(100)]
        for n, invocation in enumerate(invocations):
            invocation.will_return('BAR%d' % n)
            invocation.will_block(random.random() / 100)
        self._execute(invocations, jobs=1)
        self.assertEqual(['BAR%d' % n for n in range(100)],
                         self.stdout_stub.getvalue().splitlines())

    def test_returncode(self):
        invocation = MockInvocation()
        invocation.will_returncode(0)
        self.assertEqual(self._execute([invocation]), 0)
        invocation = MockInvocation()
        invocation.will_returncode(1)
        self.assertEqual(self._execute([invocation]), 1)
        invocation = MockInvocation()
        invocation.will_returncode(2)
        self.assertEqual(self._execute([invocation]), 2)

    def test_returncode_asynchronous(self):
        invocations = [MockInvocation() for _ in range(100)]
        for invocation in invocations:
            invocation.will_returncode(0)
            invocation.will_block(random.random() / 100)
        self.assertEqual(self._execute(invocations, jobs=100), 0)
        invocations = [MockInvocation() for _ in range(100)]
        for invocation in invocations:
            invocation.will_returncode(2)
            invocation.will_block(random.random() / 100)
        self.assertEqual(self._execute(invocations, jobs=100), 2)
        invocations = [MockInvocation() for _ in range(100)]
        for n, invocation in enumerate(invocations):
            invocation.will_returncode(6 if n == 0 else 2)
            invocation.will_block(random.random() / 100)
        self.assertEqual(self._execute(invocations, jobs=100), 6)

    def test_returncode_synchronous(self):
        invocations = [MockInvocation() for _ in range(1)]
        for invocation in invocations:
            invocation.will_returncode(0)
            invocation.will_block(random.random() / 100)
        self.assertEqual(self._execute(invocations, jobs=100), 0)
        invocations = [MockInvocation() for _ in range(1)]
        for invocation in invocations:
            invocation.will_returncode(2)
            invocation.will_block(random.random() / 100)
        self.assertEqual(self._execute(invocations, jobs=100), 2)
        invocations = [MockInvocation() for _ in range(1)]
        for n, invocation in enumerate(invocations):
            invocation.will_returncode(6 if n == 0 else 2)
            invocation.will_block(random.random() / 100)
        self.assertEqual(self._execute(invocations, jobs=100), 6)

    @unittest.skipIf(sys.platform.startswith('win'), "POSIX only")
    def test_is_subpath_of_posix(self):
        self.assertTrue(iwyu_tool.is_subpath_of('/a/b/c.c', '/a/b'))
        self.assertTrue(iwyu_tool.is_subpath_of('/a/b/c.c', '/a/b/'))
        self.assertTrue(iwyu_tool.is_subpath_of('/a/b/c.c', '/a/b/c.c'))
        self.assertFalse(iwyu_tool.is_subpath_of('/a/b/c.c', '/a/b/c'))
        self.assertFalse(iwyu_tool.is_subpath_of('/a/b/c.c', '/a/x'))
        # No case-insensitive match.
        self.assertFalse(iwyu_tool.is_subpath_of('/A/Bee/C.c', '/a/BEE'))

    @unittest.skipIf(not sys.platform.startswith('win'), "Windows only")
    def test_is_subpath_of_windows(self):
        self.assertTrue(iwyu_tool.is_subpath_of('\\a\\b\\c.c', '\\a\\b'))
        self.assertTrue(iwyu_tool.is_subpath_of('\\a\\b\\c.c', '\\a\\b\\'))
        self.assertTrue(iwyu_tool.is_subpath_of('\\a\\b\\c.c', '\\a\\b\\c.c'))
        self.assertFalse(iwyu_tool.is_subpath_of('\\a\\b\\c.c', '\\a\\b\\c'))
        self.assertFalse(iwyu_tool.is_subpath_of('\\a\\b\\c.c', '\\a\\x'))
        # Case-insensitive match.
        self.assertTrue(iwyu_tool.is_subpath_of('C:\\Bee\\C.c', 'c:\\BEE'))

    def test_from_cl_compile_command(self):
        invocation = iwyu_tool.Invocation.from_compile_command(
            {
                'directory': '/a',
                'command': 'cl.exe -I. x.cc',
                'file': 'x.cc'
            }, [])
        # Adds --driver-mode=cl if argv[0] is MSVC driver.
        self.assertEqual(
            invocation.command,
            [iwyu_tool.IWYU_EXECUTABLE, '--driver-mode=cl', '-I.', 'x.cc'])

    def test_is_msvc_driver(self):
        self.assertTrue(iwyu_tool.is_msvc_driver("cl.exe"))
        self.assertTrue(iwyu_tool.is_msvc_driver("clang-cl.exe"))
        self.assertTrue(iwyu_tool.is_msvc_driver("clang-cl"))
        self.assertFalse(iwyu_tool.is_msvc_driver("something"))

    @unittest.skipIf(not sys.platform.startswith('win'), 'Windows only')
    def test_is_msvc_driver_windows(self):
        # Case-insensitive match on Windows
        self.assertTrue(iwyu_tool.is_msvc_driver("CL.EXE"))
        self.assertTrue(iwyu_tool.is_msvc_driver("Clang-CL.exe"))
        self.assertTrue(iwyu_tool.is_msvc_driver("Clang-CL"))

    def test_split_command(self):
        self.assertEqual(['a', 'b', 'c d'],
                         iwyu_tool.split_command('a b "c d"'))

        self.assertEqual(['c', '-Idir/with spaces', 'x'],
                         iwyu_tool.split_command('c -I"dir/with spaces" x'))


class WinSplitTests(unittest.TestCase):
    """ iwyu_tool.win_split is subtle and complex enough that it warrants a
    dedicated test suite.
    """

    def assert_win_split(self, cmdstr, expected):
        self.assertEqual(expected,
                         iwyu_tool.win_split(cmdstr))

    def test_msdn_examples(self):
        """ Examples from below, detailing how to parse command-lines:
        https://msdn.microsoft.com/en-us/library/windows/desktop/17w5ykft.aspx
        """
        self.assert_win_split('"abc" d e',
                              ['abc', 'd', 'e'])
        self.assert_win_split(r'a\\b d"e f"g h',
                              [r'a\\b', 'de fg', 'h'])
        self.assert_win_split(r'a\\\"b c d',
                              [r'a\"b', 'c', 'd'])
        self.assert_win_split(r'a\\\\"b c" d e',
                              [r'a\\b c', 'd', 'e'])

        # Extra: odd number of backslashes before non-quote (should be
        # interpreted literally).
        self.assert_win_split(r'a\\\b d"e f"g h',
                              [r'a\\\b', 'de fg', 'h'])

    def test_trailing_backslash(self):
        """ Check that args with trailing backslash are retained. """
        self.assert_win_split('a\\ b c', ['a\\', 'b', 'c'])
        self.assert_win_split('a\\\\ b c', ['a\\\\', 'b', 'c'])

        # Last arg has dedicated handling, make sure backslashes are flushed.
        self.assert_win_split('b c a\\', ['b', 'c', 'a\\'])

    def test_cmake_examples(self):
        """ Example of observed CMake outputs that are hard to split. """
        self.assert_win_split(r'-I"..\tools\clang\tools\iwyu\inc ludes" -A',
                              [r'-I..\tools\clang\tools\iwyu\inc ludes', '-A'])

        self.assert_win_split(r'clang -Idir\\using\\os\\seps f.cc',
                              ['clang', r'-Idir\\using\\os\\seps', 'f.cc'])

        self.assert_win_split(r'clang -Idir\using\os\seps f.cc',
                              ['clang', r'-Idir\using\os\seps', 'f.cc'])

    def test_consecutive_spaces(self):
        """ Consecutive spaces outside of quotes should be folded. """
        self.assert_win_split('clang  -I.      -A',
                              ['clang', '-I.', '-A'])

        self.assert_win_split('clang  -I. \t     -A',
                              ['clang', '-I.', '-A'])


class BootstrapTests(unittest.TestCase):
    def setUp(self):
        self.main = MockIwyuToolMain()

    def tearDown(self):
        self.main.reset()

    def test_argparse_args(self):
        """ Argparse arguments are forwarded to main. """
        argv = ['iwyu_tool.py', '-v', '-o', 'clang', '-j', '12', '-p', '.',
                'src1', 'src2']
        iwyu_tool._bootstrap(argv)
        self.assertEqual('.', self.main.call_args['compilation_db_path'])
        self.assertEqual(['src1', 'src2'], self.main.call_args['source_files'])
        self.assertEqual(True, self.main.call_args['verbose'])
        self.assertEqual(iwyu_tool.FORMATTERS['clang'],
                         self.main.call_args['formatter'])
        self.assertEqual(12, self.main.call_args['jobs'])
        self.assertEqual([], self.main.call_args['extra_args'])

    def test_extra_args(self):
        """ Extra arguments after '--' are forwarded to main. """
        argv = ['iwyu_tool.py', '-p', '.', '--', '-extra1', '-extra2']
        iwyu_tool._bootstrap(argv)
        self.assertEqual(['-extra1', '-extra2'],
                         self.main.call_args['extra_args'])

    def test_extra_iwyu_args(self):
        """ Extra arguments with '-Xiwyu' prefix are forwarded verbatim. """
        argv = ['iwyu_tool.py', '-p', '.', '--', '-Xiwyu', '--arg']
        iwyu_tool._bootstrap(argv)
        self.assertEqual(['-Xiwyu', '--arg'], self.main.call_args['extra_args'])

    def test_extra_args_with_sep(self):
        """ If there are multiple '--' separators, subsequent ones are forwarded
        verbatim as part of extra arguments. """
        argv = ['iwyu_tool.py', '-p', '.', '--', 'arg1', '--', 'another_arg1']
        iwyu_tool._bootstrap(argv)
        self.assertEqual(['arg1', '--', 'another_arg1'],
                         self.main.call_args['extra_args'])


class CompilationDBTests(unittest.TestCase):
    def setUp(self):
        self.cwd = os.path.realpath(os.getcwd())

    def test_fixup_compilation_db(self):
        """ Compilation database path canonicalization. """
        compilation_db = [
            {
                "file": "Test.cpp"
            }
        ]

        canonical = iwyu_tool.fixup_compilation_db(compilation_db)

        # Check that file path is made absolute.
        entry = canonical[0]
        self.assertEqual(os.path.join(self.cwd, 'Test.cpp'), entry['file'])

    def test_fixup_from_entry_dir(self):
        """ Compilation database abs path is based on an entry's directory. """

        # Use a root dir from uuidgen so we don't risk hitting a real path.
        compilation_db = [
            {
                "directory": "/c057f113f69311e990bf54a05050d914/foobar",
                "file": "Test.cpp"
            }
        ]

        canonical = iwyu_tool.fixup_compilation_db(compilation_db)

        # Check that the file path is relative to the directory entry,
        # not to the current directory.
        entry = canonical[0]
        self.assertEqual('/c057f113f69311e990bf54a05050d914/foobar/Test.cpp',
                         entry['file'])

    def test_unwrap_compile_command(self):
        """ Wrapping compile commands should be unwrapped. """
        compilation_db = {
            'directory': '/home/user/llvm/build',
            "command": "ccache cc -c test.c"
        }

        invocation = iwyu_tool.Invocation.from_compile_command(compilation_db, [])

        self.assertEqual(
            invocation.command,
            [iwyu_tool.IWYU_EXECUTABLE, '-c', 'test.c'])


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