#!/usr/bin/env python3
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import pathlib
import shutil
import sys
import tempfile
import textwrap
import unittest
from unittest import mock

import gn_helpers


class UnitTest(unittest.TestCase):
    def test_ToGNString(self):
        test_cases = [
            (42, '42', '42'),
            ('foo', '"foo"', '"foo"'),
            (True, 'true', 'true'),
            (False, 'false', 'false'),
            ('', '""', '""'),
            ('\\$"$\\', '"\\\\\\$\\"\\$\\\\"', '"\\\\\\$\\"\\$\\\\"'),
            (' \t\r\n', '" $0x09$0x0D$0x0A"', '" $0x09$0x0D$0x0A"'),
            ('\u2713', '"$0xE2$0x9C$0x93"', '"$0xE2$0x9C$0x93"'),
            ([], '[  ]', '[]'),
            ([1], '[ 1 ]', '[\n  1\n]\n'),
            ([3, 1, 4, 1], '[ 3, 1, 4, 1 ]', '[\n  3,\n  1,\n  4,\n  1\n]\n'),
            (
                ['a', True, 2],
                '[ "a", true, 2 ]',
                '[\n  "a",\n  true,\n  2\n]\n',
            ),
            ({'single': 'item'}, 'single = "item"\n', 'single = "item"\n'),
            (
                {'kEy': 137, '_42A_Zaz_': [False, True]},
                '_42A_Zaz_ = [ false, true ]\nkEy = 137\n',
                '_42A_Zaz_ = [\n  false,\n  true\n]\nkEy = 137\n',
            ),
            (
                [1, 'two', ['"thr,.$\\', True, False, [], '(\u2713)']],
                '[ 1, "two", [ "\\"thr,.\\$\\\\", true, false, '
                + '[  ], "($0xE2$0x9C$0x93)" ] ]',
                '''[
  1,
  "two",
  [
    "\\"thr,.\\$\\\\",
    true,
    false,
    [],
    "($0xE2$0x9C$0x93)"
  ]
]
''',
            ),
            (
                {'s': 'foo', 'n': 42, 'b': True, 'a': [3, 'x']},
                'a = [ 3, "x" ]\nb = true\nn = 42\ns = "foo"\n',
                'a = [\n  3,\n  "x"\n]\nb = true\nn = 42\ns = "foo"\n',
            ),
            (
                [[[], [[]]], []],
                '[ [ [  ], [ [  ] ] ], [  ] ]',
                '[\n  [\n    [],\n    [\n      []\n    ]\n  ],\n  []\n]\n',
            ),
            (
                [{'a': 1, 'c': {'z': 8}, 'b': []}],
                '[ { a = 1\nb = [  ]\nc = { z = 8 } } ]\n',
                '[\n  {\n    a = 1\n    b = []\n    c = {\n'
                + '      z = 8\n    }\n  }\n]\n',
            ),
        ]
        for obj, exp_ugly, exp_pretty in test_cases:
            out_ugly = gn_helpers.ToGNString(obj)
            self.assertEqual(exp_ugly, out_ugly)
            out_pretty = gn_helpers.ToGNString(obj, pretty=True)
            self.assertEqual(exp_pretty, out_pretty)

    def test_UnescapeGNString(self):
        # Backslash followed by a \, $, or " means the folling character without
        # the special meaning. Backslash followed by everything else is a literal.
        self.assertEqual(
            gn_helpers.UnescapeGNString('\\as\\$\\\\asd\\"'), '\\as$\\asd"'
        )

    def test_FromGNString(self):
        self.assertEqual(
            gn_helpers.FromGNString('[1, -20, true, false,["as\\"", []]]'),
            [1, -20, True, False, ['as"', []]],
        )

        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('123 456')
            parser.Parse()

    def test_ParseBool(self):
        parser = gn_helpers.GNValueParser('true')
        self.assertEqual(parser.Parse(), True)

        parser = gn_helpers.GNValueParser('false')
        self.assertEqual(parser.Parse(), False)

    def test_ParseNumber(self):
        parser = gn_helpers.GNValueParser('123')
        self.assertEqual(parser.ParseNumber(), 123)

        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('')
            parser.ParseNumber()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('a123')
            parser.ParseNumber()

    def test_ParseString(self):
        parser = gn_helpers.GNValueParser('"asdf"')
        self.assertEqual(parser.ParseString(), 'asdf')

        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('')  # Empty.
            parser.ParseString()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('asdf')  # Unquoted.
            parser.ParseString()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('"trailing')  # Unterminated.
            parser.ParseString()

    def test_ParseList(self):
        parser = gn_helpers.GNValueParser('[1,]')  # Optional end comma OK.
        self.assertEqual(parser.ParseList(), [1])

        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('')  # Empty.
            parser.ParseList()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('asdf')  # No [].
            parser.ParseList()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('[1, 2')  # Unterminated
            parser.ParseList()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('[1 2]')  # No separating comma.
            parser.ParseList()

    def test_ParseScope(self):
        parser = gn_helpers.GNValueParser('{a = 1}')
        self.assertEqual(parser.ParseScope(), {'a': 1})

        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('')  # Empty.
            parser.ParseScope()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('asdf')  # No {}.
            parser.ParseScope()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('{a = 1')  # Unterminated.
            parser.ParseScope()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('{"a" = 1}')  # Not identifier.
            parser.ParseScope()
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser('{a = }')  # No value.
            parser.ParseScope()

    def test_FromGNArgs(self):
        # Booleans and numbers should work; whitespace is allowed works.
        self.assertEqual(
            gn_helpers.FromGNArgs('foo = true\nbar = 1\n'),
            {'foo': True, 'bar': 1},
        )

        # Whitespace is not required; strings should also work.
        self.assertEqual(
            gn_helpers.FromGNArgs('foo="bar baz"'), {'foo': 'bar baz'}
        )

        # Comments should work (and be ignored).
        gn_args_lines = [
            '# Top-level comment.',
            'foo = true',
            'bar = 1  # In-line comment followed by whitespace.',
            ' ',
            'baz = false',
        ]
        self.assertEqual(
            gn_helpers.FromGNArgs('\n'.join(gn_args_lines)),
            {'foo': True, 'bar': 1, 'baz': False},
        )

        # Lists should work.
        self.assertEqual(
            gn_helpers.FromGNArgs('foo=[1, 2, 3]'), {'foo': [1, 2, 3]}
        )

        # Empty strings should return an empty dict.
        self.assertEqual(gn_helpers.FromGNArgs(''), {})
        self.assertEqual(gn_helpers.FromGNArgs(' \n '), {})

        # Comments should work everywhere (and be ignored).
        gn_args_lines = [
            '# Top-level comment.',
            '',
            '# Variable comment.',
            'foo = true',
            'bar = [',
            '    # Value comment in list.',
            '    1,',
            '    2,',
            ']',
            '',
            'baz # Comment anywhere, really',
            '  = # also here',
            '    4',
        ]
        self.assertEqual(
            gn_helpers.FromGNArgs('\n'.join(gn_args_lines)),
            {'foo': True, 'bar': [1, 2], 'baz': 4},
        )

        # Scope should be parsed, even empty ones.
        gn_args_lines = [
            'foo = {',
            '  a = 1',
            '  b = [',
            '    { },',
            '    {',
            '      c = 1',
            '    },',
            '  ]',
            '}',
        ]
        self.assertEqual(
            gn_helpers.FromGNArgs('\n'.join(gn_args_lines)),
            {
                'foo': {
                    'a': 1,
                    'b': [
                        {},
                        {
                            'c': 1,
                        },
                    ],
                }
            },
        )

        # Non-identifiers should raise an exception.
        with self.assertRaises(gn_helpers.GNError):
            gn_helpers.FromGNArgs('123 = true')

        # References to other variables should raise an exception.
        with self.assertRaises(gn_helpers.GNError):
            gn_helpers.FromGNArgs('foo = bar')

        # References to functions should raise an exception.
        with self.assertRaises(gn_helpers.GNError):
            gn_helpers.FromGNArgs('foo = exec_script("//build/baz.py")')

        # Underscores in identifiers should work.
        self.assertEqual(gn_helpers.FromGNArgs('_foo = true'), {'_foo': True})
        self.assertEqual(
            gn_helpers.FromGNArgs('foo_bar = true'), {'foo_bar': True}
        )
        self.assertEqual(gn_helpers.FromGNArgs('foo_=true'), {'foo_': True})

    def test_ReplaceImports(self):
        # Should be a no-op on args inputs without any imports.
        parser = gn_helpers.GNValueParser(
            textwrap.dedent("""
        some_arg1 = "val1"
        some_arg2 = "val2"
    """)
        )
        parser.ReplaceImports()
        self.assertEqual(
            parser.input,
            textwrap.dedent("""
        some_arg1 = "val1"
        some_arg2 = "val2"
    """),
        )

        # A single "import(...)" line should be replaced with the contents of the
        # file being imported.
        parser = gn_helpers.GNValueParser(
            textwrap.dedent("""
        some_arg1 = "val1"
        import("//some/args/file.gni")
        some_arg2 = "val2"
    """)
        )
        fake_import = 'some_imported_arg = "imported_val"'
        builtin_var = (
            '__builtin__' if sys.version_info.major < 3 else 'builtins'
        )
        open_fun = '{}.open'.format(builtin_var)
        with mock.patch(open_fun, mock.mock_open(read_data=fake_import)):
            parser.ReplaceImports()
        self.assertEqual(
            parser.input,
            textwrap.dedent("""
        some_arg1 = "val1"
        some_imported_arg = "imported_val"
        some_arg2 = "val2"
    """),
        )

        # No trailing parenthesis should raise an exception.
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser(
                textwrap.dedent('import("//some/args/file.gni"')
            )
            parser.ReplaceImports()

        # No double quotes should raise an exception.
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser(
                textwrap.dedent('import(//some/args/file.gni)')
            )
            parser.ReplaceImports()

        # A path that's not source absolute should raise an exception.
        with self.assertRaises(gn_helpers.GNError):
            parser = gn_helpers.GNValueParser(
                textwrap.dedent('import("some/relative/args/file.gni")')
            )
            parser.ReplaceImports()

        if sys.platform.startswith('win32'):
            parser = gn_helpers.GNValueParser(
                textwrap.dedent("""
          some_arg1 = "val1"
          import("/c:/some/args/file.gni")
          some_arg2 = "val2"
          """)
            )
            with mock.patch(open_fun, mock.mock_open(read_data=fake_import)):
                parser.ReplaceImports()
            self.assertEqual(
                parser.input,
                textwrap.dedent("""
          some_arg1 = "val1"
          some_imported_arg = "imported_val"
          some_arg2 = "val2"
          """),
            )

            # A path that's not source absolute should raise an exception.
            with self.assertRaises(gn_helpers.GNError):
                parser = gn_helpers.GNValueParser(
                    textwrap.dedent('import("c:/some/args/file.gni")')
                )
                parser.ReplaceImports()

    @mock.patch.dict(os.environ, {"SWARMING_BOT_ID": ""})
    def test_CreateBuildCommand(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            suffix = '.bat' if sys.platform.startswith('win32') else ''
            self.assertEqual(
                f'autoninja{suffix}', gn_helpers.CreateBuildCommand(temp_dir)[0]
            )

            siso_deps = pathlib.Path(temp_dir) / '.siso_deps'
            siso_deps.touch()
            self.assertEqual(
                f'autoninja{suffix}', gn_helpers.CreateBuildCommand(temp_dir)[0]
            )

            with mock.patch('shutil.which', lambda x: None):
                cmd = gn_helpers.CreateBuildCommand(temp_dir)
                self.assertIn('third_party', cmd[0])
                self.assertIn(f'{os.sep}siso', cmd[0])
                self.assertEqual(['ninja', '-C', temp_dir], cmd[1:])

            ninja_deps = pathlib.Path(temp_dir) / '.ninja_deps'
            ninja_deps.touch()

            with self.assertRaisesRegex(Exception, 'Found both'):
                gn_helpers.CreateBuildCommand(temp_dir)

            siso_deps.unlink()
            self.assertEqual(
                f'autoninja{suffix}', gn_helpers.CreateBuildCommand(temp_dir)[0]
            )

            with mock.patch('shutil.which', lambda x: None):
                cmd = gn_helpers.CreateBuildCommand(temp_dir)
                self.assertIn('third_party', cmd[0])
                self.assertIn(f'{os.sep}ninja', cmd[0])
                self.assertEqual(['-C', temp_dir], cmd[1:])


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