# 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, 3, 56, 41), 'mock.patch', 'mock.patch', (), '', False, 'import mock\n'), ((35, 4, 35, 36), 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', ({(35, 31, 35, 35): 'self'}, {}), '(self)', False, 'from tests.test_libs import test_utils\n'), ((44, 4, 44, 37), 'system.shell.remove_empty_files', 'shell.remove_empty_files', ({(44, 29, 44, 36): '"""/test"""'}, {}), "('/test')", False, 'from system import shell\n'), ((53, 4, 53, 47), 'system.shell.remove_empty_files', 'shell.remove_empty_files', ({(53, 29, 53, 46): '"""/test/aa/cc.txt"""'}, {}), "('/test/aa/cc.txt')", False, 'from system import shell\n'), ((63, 4, 63, 37), 'system.shell.remove_empty_files', 'shell.remove_empty_files', ({(63, 29, 63, 36): '"""/test"""'}, {}), "('/test')", False, 'from system import shell\n'), ((71, 4, 80, 6), 'tests.test_libs.helpers.patch', 'test_helpers.patch', ({(71, 23, 71, 27): 'self', (71, 29, 80, 5): "['os.chmod', 'os.mkdir', 'os.path.exists', 'os.system',\n    'system.environment.platform', 'metrics.logs.log_error',\n    'metrics.logs.log_warn', 'shutil.rmtree']"}, {}), "(self, ['os.chmod', 'os.mkdir', 'os.path.exists',\n    'os.system', '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, 13, 89, 61), 'system.shell.remove_directory', 'shell.remove_directory', (), '', False, 'from system import shell\n'), ((159, 14, 159, 30), 'mock.MagicMock', 'mock.MagicMock', ({}, {}), '()', False, 'import mock\n'), ((172, 4, 172, 36), 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', ({(172, 31, 172, 35): 'self'}, {}), '(self)', False, 'from tests.test_libs import test_utils\n'), ((188, 4, 188, 36), 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', ({(188, 31, 188, 35): 'self'}, {}), '(self)', False, 'from tests.test_libs import test_utils\n'), ((216, 4, 218, 6), 'tests.test_libs.helpers.patch', 'test_helpers.patch', ({(216, 23, 216, 27): 'self', (216, 29, 218, 5): "['tempfile.gettempdir']"}, {}), "(self, ['tempfile.gettempdir'])", True, 'from tests.test_libs import helpers as test_helpers\n'), ((221, 4, 221, 36), 'tests.test_libs.test_utils.set_up_pyfakefs', 'test_utils.set_up_pyfakefs', ({(221, 31, 221, 35): 'self'}, {}), '(self)', False, 'from tests.test_libs import test_utils\n'), ((231, 4, 231, 53), 'os.symlink', 'os.symlink', ({(231, 15, 231, 34): '"""/unrelated/zz.txt"""', (231, 36, 231, 52): '"""/tmp/hh/gg.txt"""'}, {}), "('/unrelated/zz.txt', '/tmp/hh/gg.txt')", False, 'import os\n'), ((232, 4, 232, 39), 'os.symlink', 'os.symlink', ({(232, 15, 232, 27): '"""/unrelated"""', (232, 29, 232, 38): '"""/tmp/ii"""'}, {}), "('/unrelated', '/tmp/ii')", False, 'import os\n'), ((234, 4, 234, 39), 'system.shell.clear_system_temp_directory', 'shell.clear_system_temp_directory', ({}, {}), '()', False, 'from system import shell\n'), ((32, 11, 32, 33), 'system.environment.is_posix', 'environment.is_posix', ({}, {}), '()', False, 'from system import environment\n'), ((46, 20, 46, 53), 'os.path.exists', 'os.path.exists', ({(46, 35, 46, 52): '"""/test/aa/bb.txt"""'}, {}), "('/test/aa/bb.txt')", False, 'import os\n'), ((47, 20, 47, 56), 'os.path.exists', 'os.path.exists', ({(47, 35, 47, 55): '"""/test/aa/aa/dd.txt"""'}, {}), "('/test/aa/aa/dd.txt')", False, 'import os\n'), ((48, 21, 48, 54), 'os.path.exists', 'os.path.exists', ({(48, 36, 48, 53): '"""/test/aa/cc.txt"""'}, {}), "('/test/aa/cc.txt')", False, 'import os\n'), ((49, 21, 49, 57), 'os.path.exists', 'os.path.exists', ({(49, 36, 49, 56): '"""/test/aa/aa/aa.txt"""'}, {}), "('/test/aa/aa/aa.txt')", False, 'import os\n'), ((54, 20, 54, 53), 'os.path.exists', 'os.path.exists', ({(54, 35, 54, 52): '"""/test/aa/cc.txt"""'}, {}), "('/test/aa/cc.txt')", False, 'import os\n'), ((64, 20, 64, 53), 'os.path.exists', 'os.path.exists', ({(64, 35, 64, 52): '"""/test/aa/cc.txt"""'}, {}), "('/test/aa/cc.txt')", False, 'import os\n'), ((124, 20, 124, 49), 'system.shell.remove_directory', 'shell.remove_directory', ({(124, 43, 124, 48): '"""dir"""'}, {}), "('dir')", False, 'from system import shell\n'), ((132, 21, 132, 50), 'system.shell.remove_directory', 'shell.remove_directory', ({(132, 44, 132, 49): '"""dir"""'}, {}), "('dir')", False, 'from system import shell\n'), ((142, 21, 142, 70), 'system.shell.remove_directory', 'shell.remove_directory', (), '', False, 'from system import shell\n'), ((153, 20, 153, 49), 'system.shell.remove_directory', 'shell.remove_directory', ({(153, 43, 153, 48): '"""dir"""'}, {}), "('dir')", False, 'from system import shell\n'), ((181, 21, 181, 63), 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', ({(181, 52, 181, 62): '"""/test/aa"""'}, {}), "('/test/aa')", False, 'from system import shell\n'), ((197, 21, 197, 57), 'system.shell.get_directory_size', 'shell.get_directory_size', ({(197, 46, 197, 56): '"""/test/aa"""'}, {}), "('/test/aa')", False, 'from system import shell\n'), ((205, 11, 205, 33), 'system.environment.is_posix', 'environment.is_posix', ({}, {}), '()', False, 'from system import environment\n'), ((209, 32, 209, 49), 'system.shell.which', 'shell.which', ({(209, 44, 209, 48): '"""ls"""'}, {}), "('ls')", False, 'from system import shell\n'), ((236, 20, 236, 42), 'os.path.exists', 'os.path.exists', ({(236, 35, 236, 41): '"""/tmp"""'}, {}), "('/tmp')", False, 'import os\n'), ((237, 20, 237, 48), 'os.path.exists', 'os.path.exists', ({(237, 35, 237, 47): '"""/unrelated"""'}, {}), "('/unrelated')", False, 'import os\n'), ((238, 21, 238, 59), 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', ({(238, 52, 238, 58): '"""/tmp"""'}, {}), "('/tmp')", False, 'from system import shell\n'), ((239, 21, 239, 65), 'system.shell.get_directory_file_count', 'shell.get_directory_file_count', ({(239, 52, 239, 64): '"""/unrelated"""'}, {}), "('/unrelated')", False, 'from system import shell\n'), ((240, 21, 240, 53), 'os.path.exists', 'os.path.exists', ({(240, 36, 240, 52): '"""/tmp/aa/bb.txt"""'}, {}), "('/tmp/aa/bb.txt')", False, 'import os\n'), ((241, 21, 241, 56), 'os.path.exists', 'os.path.exists', ({(241, 36, 241, 55): '"""/tmp/cc/dd/ee.txt"""'}, {}), "('/tmp/cc/dd/ee.txt')", False, 'import os\n'), ((242, 21, 242, 49), 'os.path.exists', 'os.path.exists', ({(242, 36, 242, 48): '"""/tmp/ff/gg"""'}, {}), "('/tmp/ff/gg')", False, 'import os\n'), ((243, 21, 243, 46), 'os.path.exists', 'os.path.exists', ({(243, 36, 243, 45): '"""/tmp/hh"""'}, {}), "('/tmp/hh')", False, 'import os\n'), ((253, 21, 253, 63), 'system.shell.get_execute_command', 'shell.get_execute_command', ({(253, 47, 253, 62): 'file_to_execute'}, {}), '(file_to_execute)', False, 'from system import shell\n'), ((281, 31, 281, 62), 'system.shell.get_interpreter', 'shell.get_interpreter', ({(281, 53, 281, 61): '"""run.py"""'}, {}), "('run.py')", False, 'from system import shell\n'), ((286, 22, 286, 57), 'system.shell.get_interpreter', 'shell.get_interpreter', ({(286, 44, 286, 56): '"""executable"""'}, {}), "('executable')", False, 'from system import shell\n'), ((105, 39, 105, 77), 'mock.call', 'mock.call', ({(105, 49, 105, 76): '"""rd /s /q "dir" > nul 2>&1"""'}, {}), '(\'rd /s /q "dir" > nul 2>&1\')', False, 'import mock\n'), ((111, 9, 111, 51), 'mock.call', 'mock.call', ({(111, 19, 111, 50): '"""rm -rf "dir" > /dev/null 2>&1"""'}, {}), '(\'rm -rf "dir" > /dev/null 2>&1\')', False, 'import mock\n'), ((126, 9, 126, 51), 'mock.call', 'mock.call', ({(126, 19, 126, 50): '"""rm -rf "dir" > /dev/null 2>&1"""'}, {}), '(\'rm -rf "dir" > /dev/null 2>&1\')', False, 'import mock\n'), ((127, 39, 127, 73), 'mock.call', 'mock.call', (), '', False, 'import mock\n'), ((134, 9, 134, 52), 'mock.call', 'mock.call', ({(134, 19, 134, 51): '"""Failed to clear directory dir."""'}, {}), "('Failed to clear directory dir.')", False, 'import mock\n'), ((137, 9, 137, 51), 'mock.call', 'mock.call', ({(137, 19, 137, 50): '"""rm -rf "dir" > /dev/null 2>&1"""'}, {}), '(\'rm -rf "dir" > /dev/null 2>&1\')', False, 'import mock\n'), ((138, 39, 138, 73), 'mock.call', 'mock.call', (), '', False, 'import mock\n'), ((144, 9, 144, 52), 'mock.call', 'mock.call', ({(144, 19, 144, 51): '"""Failed to clear directory dir."""'}, {}), "('Failed to clear directory dir.')", False, 'import mock\n'), ((147, 9, 147, 51), 'mock.call', 'mock.call', ({(147, 19, 147, 50): '"""rm -rf "dir" > /dev/null 2>&1"""'}, {}), '(\'rm -rf "dir" > /dev/null 2>&1\')', False, 'import mock\n'), ((148, 39, 148, 73), 'mock.call', 'mock.call', (), '', False, 'import mock\n'), ((155, 9, 155, 51), 'mock.call', 'mock.call', ({(155, 19, 155, 50): '"""rm -rf "dir" > /dev/null 2>&1"""'}, {}), '(\'rm -rf "dir" > /dev/null 2>&1\')', False, 'import mock\n'), ((156, 39, 156, 73), 'mock.call', 'mock.call', (), '', False, 'import mock\n'), ((164, 38, 164, 67), 'mock.call', 'mock.call', ({(164, 48, 164, 59): '"""dir/child"""', (164, 61, 164, 66): '(488)'}, {}), "('dir/child', 488)", False, 'import mock\n'), ((165, 30, 165, 52), 'mock.call', 'mock.call', ({(165, 40, 165, 51): '"""dir/child"""'}, {}), "('dir/child')", False, 'import mock\n'), ((98, 40, 98, 56), 'mock.call', 'mock.call', ({(98, 50, 98, 55): '"""dir"""'}, {}), "('dir')", False, 'import mock\n')]