# 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', False, 'import mock\n'), (35, 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', False, 'from tests.test_libs import test_utils\n'), (44, 'system.shell.remove_empty_files', 'shell.remove_empty_files', False, 'from system import shell\n'), (53, 'system.shell.remove_empty_files', 'shell.remove_empty_files', False, 'from system import shell\n'), (63, 'system.shell.remove_empty_files', 'shell.remove_empty_files', False, 'from system import shell\n'), (71, 'tests.test_libs.helpers.patch', 'test_helpers.patch', True, 'from tests.test_libs import helpers as test_helpers\n'), (89, 'system.shell.remove_directory', 'shell.remove_directory', 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', False, 'from tests.test_libs import test_utils\n'), (188, 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', False, 'from tests.test_libs import test_utils\n'), (216, 'tests.test_libs.helpers.patch', 'test_helpers.patch', 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', False, 'from tests.test_libs import test_utils\n'), (231, 'os.symlink', 'os.symlink', False, 'import os\n'), (232, 'os.symlink', 'os.symlink', 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', False, 'import os\n'), (47, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (48, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (49, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (54, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (64, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (124, 'system.shell.remove_directory', 'shell.remove_directory', False, 'from system import shell\n'), (132, 'system.shell.remove_directory', 'shell.remove_directory', False, 'from system import shell\n'), (142, 'system.shell.remove_directory', 'shell.remove_directory', False, 'from system import shell\n'), (153, 'system.shell.remove_directory', 'shell.remove_directory', False, 'from system import shell\n'), (181, 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', False, 'from system import shell\n'), (197, 'system.shell.get_directory_size', 'shell.get_directory_size', 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', False, 'from system import shell\n'), (236, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (237, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (238, 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', False, 'from system import shell\n'), (239, 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', False, 'from system import shell\n'), (240, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (241, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (242, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (243, 'os.path.exists', 'os.path.exists', False, 'import os\n'), (253, 'system.shell.get_execute_command', 'shell.get_execute_command', False, 'from system import shell\n'), (281, 'system.shell.get_interpreter', 'shell.get_interpreter', False, 'from system import shell\n'), (286, 'system.shell.get_interpreter', 'shell.get_interpreter', False, 'from system import shell\n'), (105, 'mock.call', 'mock.call', False, 'import mock\n'), (111, 'mock.call', 'mock.call', False, 'import mock\n'), (126, 'mock.call', 'mock.call', False, 'import mock\n'), (127, 'mock.call', 'mock.call', False, 'import mock\n'), (134, 'mock.call', 'mock.call', False, 'import mock\n'), (137, 'mock.call', 'mock.call', False, 'import mock\n'), (138, 'mock.call', 'mock.call', False, 'import mock\n'), (144, 'mock.call', 'mock.call', False, 'import mock\n'), (147, 'mock.call', 'mock.call', False, 'import mock\n'), (148, 'mock.call', 'mock.call', False, 'import mock\n'), (155, 'mock.call', 'mock.call', False, 'import mock\n'), (156, 'mock.call', 'mock.call', False, 'import mock\n'), (164, 'mock.call', 'mock.call', False, 'import mock\n'), (165, 'mock.call', 'mock.call', False, 'import mock\n'), (98, 'mock.call', 'mock.call', False, 'import mock\n')]