# Copyright 2019 Google LLC
#
# 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.
"""shell tests."""
import mock
import os
import unittest

from pyfakefs import fake_filesystem_unittest

from system import environment
from system import shell
from tests.test_libs import helpers as test_helpers
from tests.test_libs import test_utils


class RemoveEmptyFilesTest(fake_filesystem_unittest.TestCase):
  """Tests for remove_empty_files."""

  def setUp(self):
    # FIXME: Add support for Windows.
    if not environment.is_posix():
      self.skipTest('Process tests are only applicable for posix platforms.')

    test_utils.set_up_pyfakefs(self)

  def test_remove(self):
    """Test remove."""
    self.fs.CreateFile('/test/aa/bb.txt', contents='s')
    self.fs.CreateFile('/test/aa/cc.txt', contents='')
    self.fs.CreateFile('/test/aa/aa/dd.txt', contents='s')
    self.fs.CreateFile('/test/aa/aa/aa.txt', contents='')

    shell.remove_empty_files('/test')

    self.assertTrue(os.path.exists('/test/aa/bb.txt'))
    self.assertTrue(os.path.exists('/test/aa/aa/dd.txt'))
    self.assertFalse(os.path.exists('/test/aa/cc.txt'))
    self.assertFalse(os.path.exists('/test/aa/aa/aa.txt'))

  def test_ignore_file(self):
    self.fs.CreateFile('/test/aa/cc.txt', contents='')
    shell.remove_empty_files('/test/aa/cc.txt')
    self.assertTrue(os.path.exists('/test/aa/cc.txt'))

  @mock.patch('os.remove', autospec=True)
  def test_exception(self, mock_remove):
    # bypass pyfakefs's os.remove.
    os.remove = mock_remove
    mock_remove.side_effect = OSError()

    self.fs.CreateFile('/test/aa/cc.txt', contents='')
    shell.remove_empty_files('/test')
    self.assertTrue(os.path.exists('/test/aa/cc.txt'))


class RemoveDirectoryTest(unittest.TestCase):
  """Tests for remove_directory."""

  def setUp(self):
    test_helpers.patch(self, [
        'os.chmod',
        'os.mkdir',
        'os.path.exists',
        'os.system',
        'system.environment.platform',
        'metrics.logs.log_error',
        'metrics.logs.log_warn',
        'shutil.rmtree',
    ])

  def _test_remove_os_specific(self, platform, recreate, raise_mkdir_error):
    """Helper for testing removing dir with os-specific command."""
    self.mock.platform.return_value = platform
    self.mock.exists.side_effect = [True, False, False]
    if raise_mkdir_error:
      self.mock.mkdir.side_effect = OSError()

    result = shell.remove_directory('dir', recreate=recreate)

    if recreate:
      self.assertEqual(not raise_mkdir_error, result)
    else:
      self.assertTrue(result)

    self.mock.rmtree.assert_has_calls([])
    if recreate:
      self.mock.mkdir.assert_has_calls([mock.call('dir')])
    else:
      self.mock.mkdir.assert_has_calls([])

  def test_remove_os_specific_windows(self):
    """Test remove with os-specific command on windows."""
    self._test_remove_os_specific('WINDOWS', True, False)
    self.mock.system.assert_has_calls([mock.call('rd /s /q "dir" > nul 2>&1')])

  def test_remove_os_specific_non_windows(self):
    """Test remove with os-specific command on non-windows."""
    self._test_remove_os_specific('LINUX', True, False)
    self.mock.system.assert_has_calls(
        [mock.call('rm -rf "dir" > /dev/null 2>&1')])

  def test_remove_without_recreate(self):
    """Test remove without recreate."""
    self._test_remove_os_specific('LINUX', False, True)

  def test_remove_with_mkdir_error(self):
    """Test remove when mkdir errors."""
    self._test_remove_os_specific('LINUX', True, True)

  def test_remove_shutil_success(self):
    """Test remove with shutil."""
    self.mock.exists.side_effect = [True, True, False]
    self.assertTrue(shell.remove_directory('dir'))
    self.mock.system.assert_has_calls(
        [mock.call('rm -rf "dir" > /dev/null 2>&1')])
    self.mock.rmtree.assert_has_calls([mock.call('dir', onerror=mock.ANY)])

  def test_remove_shutil_failure(self):
    """Test remove with shutil but fails."""
    self.mock.exists.side_effect = [True, True, True]
    self.assertFalse(shell.remove_directory('dir'))
    self.mock.log_error.assert_has_calls(
        [mock.call('Failed to clear directory dir.')])
    self.assertEqual(0, self.mock.log_warn.call_count)
    self.mock.system.assert_has_calls(
        [mock.call('rm -rf "dir" > /dev/null 2>&1')])
    self.mock.rmtree.assert_has_calls([mock.call('dir', onerror=mock.ANY)])

  def test_remove_shutil_failure_ignore_errors(self):
    self.mock.exists.side_effect = [True, True, True]
    self.assertFalse(shell.remove_directory('dir', ignore_errors=True))
    self.mock.log_warn.assert_has_calls(
        [mock.call('Failed to clear directory dir.')])
    self.assertEqual(0, self.mock.log_error.call_count)
    self.mock.system.assert_has_calls(
        [mock.call('rm -rf "dir" > /dev/null 2>&1')])
    self.mock.rmtree.assert_has_calls([mock.call('dir', onerror=mock.ANY)])

  def test_remove_shutil_onerror(self):
    """Test shutil invoking onerror."""
    self.mock.exists.side_effect = [True, True, False]
    self.assertTrue(shell.remove_directory('dir'))
    self.mock.system.assert_has_calls(
        [mock.call('rm -rf "dir" > /dev/null 2>&1')])
    self.mock.rmtree.assert_has_calls([mock.call('dir', onerror=mock.ANY)])

    onerror = self.mock.rmtree.call_args[1]['onerror']
    fake_fn = mock.MagicMock()
    fake_fn.side_effect = OSError()

    onerror(fake_fn, 'dir/child', ImportError())

    self.mock.chmod.assert_has_calls([mock.call('dir/child', 0o750)])
    fake_fn.assert_has_calls([mock.call('dir/child')])


class GetDirectoryFileCount(fake_filesystem_unittest.TestCase):
  """Tests for get_directory_file_count."""

  def setUp(self):
    test_utils.set_up_pyfakefs(self)

  def test(self):
    """Test get_directory_file_count."""
    self.fs.CreateFile('/test/aa/bb.txt', contents='abc')
    self.fs.CreateFile('/test/aa/cc.txt', contents='def')
    self.fs.CreateFile('/test/aa/aa/aa.txt', contents='ghi')
    self.fs.CreateFile('/test/aa/aa/dd.txt', contents='t')

    self.assertEqual(shell.get_directory_file_count('/test/aa'), 4)


class GetDirectorySizeTest(fake_filesystem_unittest.TestCase):
  """Tests for get_directory_size."""

  def setUp(self):
    test_utils.set_up_pyfakefs(self)

  def test(self):
    """Test get_directory_size."""
    self.fs.CreateFile('/test/aa/bb.txt', contents='abc')
    self.fs.CreateFile('/test/aa/cc.txt', contents='def')
    self.fs.CreateFile('/test/aa/aa/aa.txt', contents='ghi')
    self.fs.CreateFile('/test/aa/aa/dd.txt', contents='t')

    self.assertEqual(shell.get_directory_size('/test/aa'), 10)


class WhichTest(fake_filesystem_unittest.TestCase):
  """Tests for which (shutil.which)."""

  def setUp(self):
    # FIXME: Add support for Windows.
    if not environment.is_posix():
      self.skipTest('Which test is only supported on posix platforms.')

  def test(self):
    self.assertEqual('/bin/ls', shell.which('ls'))


class ClearSystemTempDirectoryTest(fake_filesystem_unittest.TestCase):
  """Tests for clear_system_temp_directory."""

  def setUp(self):
    test_helpers.patch(self, [
        'tempfile.gettempdir',
    ])
    self.mock.gettempdir.return_value = '/tmp'

    test_utils.set_up_pyfakefs(self)

  def test(self):
    """Test clear_system_temp_directory works as expected."""
    self.fs.CreateFile('/tmp/aa/bb.txt', contents='abc')
    self.fs.CreateFile('/tmp/cc/dd/ee.txt', contents='def')
    self.fs.CreateDirectory('/tmp/ff/gg')
    self.fs.CreateDirectory('/tmp/hh')
    self.fs.CreateDirectory('/unrelated')
    self.fs.CreateFile('/unrelated/zz.txt', contents='zzz')
    os.symlink('/unrelated/zz.txt', '/tmp/hh/gg.txt')
    os.symlink('/unrelated', '/tmp/ii')

    shell.clear_system_temp_directory()

    self.assertTrue(os.path.exists('/tmp'))
    self.assertTrue(os.path.exists('/unrelated'))
    self.assertEqual(shell.get_directory_file_count('/tmp'), 0)
    self.assertEqual(shell.get_directory_file_count('/unrelated'), 1)
    self.assertFalse(os.path.exists('/tmp/aa/bb.txt'))
    self.assertFalse(os.path.exists('/tmp/cc/dd/ee.txt'))
    self.assertFalse(os.path.exists('/tmp/ff/gg'))
    self.assertFalse(os.path.exists('/tmp/hh'))


class GetExecuteCommand(unittest.TestCase):
  """Test that the correct commands to run files are returned."""

  def call_and_assert_helper(self, expected_command, file_to_execute):
    """Call get_execute_command on |file_to_execute| and assert result equal to
    |expected_command|."""
    self.assertEqual(expected_command,
                     shell.get_execute_command(file_to_execute))

  def test_standard_script(self):
    """Test correct command returned for python script."""
    script_name = 'script.py'
    expected_command = 'python %s' % script_name
    self.call_and_assert_helper(expected_command, script_name)

  def test_java(self):
    """Test correct launch command returned for Java class."""
    script_name = 'javaclassfile.class'
    expected_command = 'java javaclassfile'
    self.call_and_assert_helper(expected_command, script_name)

  def test_binary(self):
    """Test correct launch command returned for a binary (executable) file."""
    executable_name = 'executable'
    self.call_and_assert_helper(executable_name, executable_name)

    executable_name += '.exe'
    self.call_and_assert_helper(executable_name, executable_name)


class GetInterpreter(object):
  """Test that the correct interpreters to execute a file are returned."""

  def get_interpreted_file_test(self):
    """Test correct interpreter is returned for a file that needs one."""
    self.assertEqual('python', shell.get_interpreter('run.py'))

  def get_non_interpreter_file_test(self):
    """Test that None is returned for a file that doesn't need one. We don't
    want empty string since this is easier to than None. """
    self.assertIsNone(shell.get_interpreter('executable'))

[(56, 'mock.patch', 'mock.patch', (['"""os.remove"""'], {'autospec': '(True)'}), False, 'import mock\n'), (35, 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', (['self'], {}), False, 'from tests.test_libs import test_utils\n'), (44, 'system.shell.remove_empty_files', 'shell.remove_empty_files', (['"""/test"""'], {}), False, 'from system import shell\n'), (53, 'system.shell.remove_empty_files', 'shell.remove_empty_files', (['"""/test/aa/cc.txt"""'], {}), False, 'from system import shell\n'), (63, 'system.shell.remove_empty_files', 'shell.remove_empty_files', (['"""/test"""'], {}), False, 'from system import shell\n'), (71, 'tests.test_libs.helpers.patch', 'test_helpers.patch', (['self', "['os.chmod', 'os.mkdir', 'os.path.exists', 'os.system',\n    'system.environment.platform', 'metrics.logs.log_error',\n    'metrics.logs.log_warn', 'shutil.rmtree']"], {}), True, 'from tests.test_libs import helpers as test_helpers\n'), (89, 'system.shell.remove_directory', 'shell.remove_directory', (['"""dir"""'], {'recreate': 'recreate'}), False, 'from system import shell\n'), (159, 'mock.MagicMock', 'mock.MagicMock', ([], {}), False, 'import mock\n'), (172, 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', (['self'], {}), False, 'from tests.test_libs import test_utils\n'), (188, 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', (['self'], {}), False, 'from tests.test_libs import test_utils\n'), (216, 'tests.test_libs.helpers.patch', 'test_helpers.patch', (['self', "['tempfile.gettempdir']"], {}), True, 'from tests.test_libs import helpers as test_helpers\n'), (221, 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', (['self'], {}), False, 'from tests.test_libs import test_utils\n'), (231, 'os.symlink', 'os.symlink', (['"""/unrelated/zz.txt"""', '"""/tmp/hh/gg.txt"""'], {}), False, 'import os\n'), (232, 'os.symlink', 'os.symlink', (['"""/unrelated"""', '"""/tmp/ii"""'], {}), False, 'import os\n'), (234, 'system.shell.clear_system_temp_directory', 'shell.clear_system_temp_directory', ([], {}), False, 'from system import shell\n'), (32, 'system.environment.is_posix', 'environment.is_posix', ([], {}), False, 'from system import environment\n'), (46, 'os.path.exists', 'os.path.exists', (['"""/test/aa/bb.txt"""'], {}), False, 'import os\n'), (47, 'os.path.exists', 'os.path.exists', (['"""/test/aa/aa/dd.txt"""'], {}), False, 'import os\n'), (48, 'os.path.exists', 'os.path.exists', (['"""/test/aa/cc.txt"""'], {}), False, 'import os\n'), (49, 'os.path.exists', 'os.path.exists', (['"""/test/aa/aa/aa.txt"""'], {}), False, 'import os\n'), (54, 'os.path.exists', 'os.path.exists', (['"""/test/aa/cc.txt"""'], {}), False, 'import os\n'), (64, 'os.path.exists', 'os.path.exists', (['"""/test/aa/cc.txt"""'], {}), False, 'import os\n'), (124, 'system.shell.remove_directory', 'shell.remove_directory', (['"""dir"""'], {}), False, 'from system import shell\n'), (132, 'system.shell.remove_directory', 'shell.remove_directory', (['"""dir"""'], {}), False, 'from system import shell\n'), (142, 'system.shell.remove_directory', 'shell.remove_directory', (['"""dir"""'], {'ignore_errors': '(True)'}), False, 'from system import shell\n'), (153, 'system.shell.remove_directory', 'shell.remove_directory', (['"""dir"""'], {}), False, 'from system import shell\n'), (181, 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', (['"""/test/aa"""'], {}), False, 'from system import shell\n'), (197, 'system.shell.get_directory_size', 'shell.get_directory_size', (['"""/test/aa"""'], {}), False, 'from system import shell\n'), (205, 'system.environment.is_posix', 'environment.is_posix', ([], {}), False, 'from system import environment\n'), (209, 'system.shell.which', 'shell.which', (['"""ls"""'], {}), False, 'from system import shell\n'), (236, 'os.path.exists', 'os.path.exists', (['"""/tmp"""'], {}), False, 'import os\n'), (237, 'os.path.exists', 'os.path.exists', (['"""/unrelated"""'], {}), False, 'import os\n'), (238, 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', (['"""/tmp"""'], {}), False, 'from system import shell\n'), (239, 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', (['"""/unrelated"""'], {}), False, 'from system import shell\n'), (240, 'os.path.exists', 'os.path.exists', (['"""/tmp/aa/bb.txt"""'], {}), False, 'import os\n'), (241, 'os.path.exists', 'os.path.exists', (['"""/tmp/cc/dd/ee.txt"""'], {}), False, 'import os\n'), (242, 'os.path.exists', 'os.path.exists', (['"""/tmp/ff/gg"""'], {}), False, 'import os\n'), (243, 'os.path.exists', 'os.path.exists', (['"""/tmp/hh"""'], {}), False, 'import os\n'), (253, 'system.shell.get_execute_command', 'shell.get_execute_command', (['file_to_execute'], {}), False, 'from system import shell\n'), (281, 'system.shell.get_interpreter', 'shell.get_interpreter', (['"""run.py"""'], {}), False, 'from system import shell\n'), (286, 'system.shell.get_interpreter', 'shell.get_interpreter', (['"""executable"""'], {}), False, 'from system import shell\n'), (105, 'mock.call', 'mock.call', (['"""rd /s /q "dir" > nul 2>&1"""'], {}), False, 'import mock\n'), (111, 'mock.call', 'mock.call', (['"""rm -rf "dir" > /dev/null 2>&1"""'], {}), False, 'import mock\n'), (126, 'mock.call', 'mock.call', (['"""rm -rf "dir" > /dev/null 2>&1"""'], {}), False, 'import mock\n'), (127, 'mock.call', 'mock.call', (['"""dir"""'], {'onerror': 'mock.ANY'}), False, 'import mock\n'), (134, 'mock.call', 'mock.call', (['"""Failed to clear directory dir."""'], {}), False, 'import mock\n'), (137, 'mock.call', 'mock.call', (['"""rm -rf "dir" > /dev/null 2>&1"""'], {}), False, 'import mock\n'), (138, 'mock.call', 'mock.call', (['"""dir"""'], {'onerror': 'mock.ANY'}), False, 'import mock\n'), (144, 'mock.call', 'mock.call', (['"""Failed to clear directory dir."""'], {}), False, 'import mock\n'), (147, 'mock.call', 'mock.call', (['"""rm -rf "dir" > /dev/null 2>&1"""'], {}), False, 'import mock\n'), (148, 'mock.call', 'mock.call', (['"""dir"""'], {'onerror': 'mock.ANY'}), False, 'import mock\n'), (155, 'mock.call', 'mock.call', (['"""rm -rf "dir" > /dev/null 2>&1"""'], {}), False, 'import mock\n'), (156, 'mock.call', 'mock.call', (['"""dir"""'], {'onerror': 'mock.ANY'}), False, 'import mock\n'), (164, 'mock.call', 'mock.call', (['"""dir/child"""', '(488)'], {}), False, 'import mock\n'), (165, 'mock.call', 'mock.call', (['"""dir/child"""'], {}), False, 'import mock\n'), (98, 'mock.call', 'mock.call', (['"""dir"""'], {}), False, 'import mock\n')]