blob: 625082f5b11064616e624892423e52561ba1c971 [file] [log] [blame]
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
from __future__ import absolute_import, unicode_literals
import collections
import sys
import unittest
from build_swift import shell
import six
from six import StringIO
from .. import utils
try:
# Python 3.4
from pathlib import Path
except ImportError:
pass
try:
# Python 3.3
from unittest import mock
from unittest.mock import patch, mock_open, MagicMock
except ImportError:
mock, mock_open = None, None
class MagicMock(object):
def __init__(self, *args, **kwargs):
pass
def _id(obj):
return obj
def patch(*args, **kwargs):
return _id
# -----------------------------------------------------------------------------
# Constants
_OPEN_NAME = '{}.open'.format(six.moves.builtins.__name__)
# -----------------------------------------------------------------------------
# Test Cases
class TestHelpers(unittest.TestCase):
"""Unit tests for the helper functions defined in the build_swift.shell
module.
"""
# -------------------------------------------------------------------------
# _flatmap
def test_flatmap(self):
def duplicate(x):
return [x, x]
result = shell._flatmap(duplicate, [1, 2, 3])
self.assertIsInstance(result, collections.Iterable)
self.assertEqual(list(result), [1, 1, 2, 2, 3, 3])
# -------------------------------------------------------------------------
# _convert_pathlib_path
@utils.requires_module('unittest.mock')
@utils.requires_module('pathlib')
@patch('build_swift.shell.Path', None)
def test_convert_pathlib_path_pathlib_not_imported(self):
path = Path('/path/to/file.txt')
self.assertEqual(shell._convert_pathlib_path(path), path)
@utils.requires_module('pathlib')
def test_convert_pathlib_path(self):
path = Path('/path/to/file.txt')
self.assertEqual(shell._convert_pathlib_path(''), '')
self.assertEqual(
shell._convert_pathlib_path(path),
six.text_type(path))
# -------------------------------------------------------------------------
# _get_stream_file
def test_get_stream_file(self):
self.assertEqual(shell._get_stream_file(shell.PIPE), sys.stdout)
self.assertEqual(shell._get_stream_file(shell.STDOUT), sys.stdout)
self.assertEqual(shell._get_stream_file(sys.stdout), sys.stdout)
self.assertEqual(shell._get_stream_file(sys.stderr), sys.stderr)
def test_get_stream_file_raises_devnull(self):
with self.assertRaises(ValueError):
shell._get_stream_file(shell.DEVNULL)
# -------------------------------------------------------------------------
# _echo_command
@utils.requires_module('unittest.mock')
def test_echo_command(self):
test_command = ['sudo', 'rm', '-rf', '/tmp/*']
mock_stream = MagicMock()
shell._echo_command(test_command, mock_stream)
mock_stream.write.assert_called_with(
'>>> {}\n'.format(shell.quote(test_command)))
assert(mock_stream.flush.called)
@utils.requires_module('unittest.mock')
def test_echo_command_custom_prefix(self):
mock_stream = MagicMock()
shell._echo_command('ls', mock_stream, prefix='$ ')
mock_stream.write.assert_called_with('$ ls\n')
assert(mock_stream.flush.called)
# -------------------------------------------------------------------------
# _normalize_args
def test_normalize_args_splits_basestring(self):
command = 'rm -rf /Applications/Xcode.app'
self.assertEqual(
shell._normalize_args(command),
['rm', '-rf', '/Applications/Xcode.app'])
def test_normalize_args_list_str(self):
command = ['rm', '-rf', '/Applications/Xcode.app']
self.assertEqual(shell._normalize_args(command), command)
def test_normalize_args_converts_wrappers(self):
sudo = shell.wraps('sudo')
rm = shell.wraps('rm')
command = [sudo, rm, '-rf', '/Applications/Xcode.app']
self.assertEqual(
shell._normalize_args(command),
['sudo', 'rm', '-rf', '/Applications/Xcode.app'])
def test_normalize_args_converts_complex_wrapper_commands(self):
sudo_rm_rf = shell.wraps('sudo rm -rf')
command = [sudo_rm_rf, '/Applications/Xcode.app']
self.assertEqual(
shell._normalize_args(command),
['sudo', 'rm', '-rf', '/Applications/Xcode.app'])
@utils.requires_module('pathlib')
def test_normalize_args_accepts_single_wrapper_arg(self):
rm_xcode = shell.wraps(['rm', '-rf', Path('/Applications/Xcode.app')])
self.assertEqual(
shell._normalize_args(rm_xcode),
['rm', '-rf', '/Applications/Xcode.app'])
@utils.requires_module('pathlib')
def test_normalize_args_converts_pathlib_path(self):
command = ['rm', '-rf', Path('/Applications/Xcode.app')]
self.assertEqual(
shell._normalize_args(command),
['rm', '-rf', '/Applications/Xcode.app'])
@utils.requires_module('pathlib')
def test_normalize_args_converts_pathlib_path_in_wrapper_commands(self):
rm_xcode = shell.wraps(['rm', '-rf', Path('/Applications/Xcode.app')])
self.assertEqual(
shell._normalize_args([rm_xcode]),
['rm', '-rf', '/Applications/Xcode.app'])
class TestDecorators(unittest.TestCase):
"""Unit tests for the decorators defined in the build_swift.shell module
used to backport or add functionality to the subprocess wrappers.
"""
# -------------------------------------------------------------------------
# _backport_devnull
@utils.requires_module('unittest.mock')
@patch(_OPEN_NAME, new_callable=mock_open)
@patch('build_swift.shell._PY_VERSION', (3, 2))
def test_backport_devnull_stdout_kwarg(self, mock_open):
mock_file = MagicMock()
mock_open.return_value.__enter__.return_value = mock_file
@shell._backport_devnull
def func(command, **kwargs):
self.assertEqual(kwargs['stdout'], mock_file)
func('', stdout=shell.DEVNULL)
assert(mock_open.return_value.__enter__.called)
assert(mock_open.return_value.__exit__.called)
@utils.requires_module('unittest.mock')
@patch(_OPEN_NAME, new_callable=mock_open)
@patch('build_swift.shell._PY_VERSION', (3, 2))
def test_backport_devnull_stderr_kwarg(self, mock_open):
mock_file = MagicMock()
mock_open.return_value.__enter__.return_value = mock_file
@shell._backport_devnull
def func(command, **kwargs):
self.assertEqual(kwargs['stderr'], mock_file)
func('', stderr=shell.DEVNULL)
assert(mock_open.return_value.__enter__.called)
assert(mock_open.return_value.__exit__.called)
@utils.requires_module('unittest.mock')
@patch(_OPEN_NAME, new_callable=mock_open)
def test_backport_devnull_does_not_open(self, mock_open):
@shell._backport_devnull
def func(command):
pass
func('')
mock_open.return_value.__enter__.assert_not_called()
mock_open.return_value.__exit__.assert_not_called()
@utils.requires_module('unittest.mock')
@patch('build_swift.shell._PY_VERSION', (3, 3))
def test_backport_devnull_noop_starting_with_python_3_3(self):
def func():
pass
self.assertEqual(shell._backport_devnull(func), func)
# -------------------------------------------------------------------------
# _normalize_command
def test_normalize_command_basestring_command_noop(self):
test_command = 'touch test.txt'
@shell._normalize_command
def func(command):
self.assertEqual(command, test_command)
func(test_command)
@utils.requires_module('unittest.mock')
@patch('build_swift.shell._normalize_args')
def test_normalize_command(self, mock_normalize_args):
test_command = ['rm', '-rf', '/tmp/*']
@shell._normalize_command
def func(command):
pass
func(test_command)
mock_normalize_args.assert_called_with(test_command)
# -------------------------------------------------------------------------
# _add_echo_kwarg
@utils.requires_module('unittest.mock')
@patch('build_swift.shell._echo_command')
def test_add_echo_kwarg_calls_echo_command(self, mock_echo_command):
test_command = ['rm', '-rf', '/tmp/*']
@shell._add_echo_kwarg
def func(command, **kwargs):
pass
mock_stream = mock.mock_open()
func(test_command, echo=True, stdout=mock_stream)
mock_echo_command.assert_called_with(test_command, mock_stream)
@utils.requires_module('unittest.mock')
@patch('build_swift.shell._echo_command')
def test_add_echo_kwarg_noop_echo_false(self, mock_echo_command):
test_command = ['rm', '-rf', '/tmp/*']
@shell._add_echo_kwarg
def func(command):
pass
func(test_command)
func(test_command, echo=False)
mock_echo_command.assert_not_called()
class TestPublicFunctions(unittest.TestCase):
"""Unit tests for the public functions defined in the build_swift.shell
module.
"""
# -------------------------------------------------------------------------
# quote
def test_quote_string(self):
self.assertEqual(
shell.quote('/Applications/App Store.app'),
"'/Applications/App Store.app'")
def test_quote_iterable(self):
self.assertEqual(
shell.quote(['rm', '-rf', '~/Documents/My Homework']),
"rm -rf '~/Documents/My Homework'")
# -------------------------------------------------------------------------
# rerun_as_root
def test_rerun_as_root(self):
pass
class TestSubprocessWrappers(unittest.TestCase):
"""Unit tests for the subprocess wrappers defined in the build_swift.shell
module.
"""
# -------------------------------------------------------------------------
# Popen
# NOTE: Testing the Popen class is harder than it might appear. We're not
# able to mock out the subprocess.Popen superclass as one might initially
# expect. Rather that shell.Popen class object already exists and inherts
# from subprocess.Popen, thus mocking it out does not change the behavior.
# Ultimately this class is merely a wrapper that uses already tested
# decorators to add functionality so testing here might not provide any
# benefit.
# -------------------------------------------------------------------------
# call
@utils.requires_module('unittest.mock')
@patch('subprocess.call')
def test_call(self, mock_call):
shell.call('ls')
mock_call.assert_called_with('ls')
# -------------------------------------------------------------------------
# check_call
@utils.requires_module('unittest.mock')
@patch('subprocess.check_call')
def test_check_call(self, mock_check_call):
shell.check_call('ls')
mock_check_call.assert_called_with('ls')
# -------------------------------------------------------------------------
# check_output
@utils.requires_module('unittest.mock')
@patch('subprocess.check_output')
def test_check_output(self, mock_check_output):
# Before Python 3 the subprocess.check_output function returned bytes.
if six.PY3:
mock_check_output.return_value = ''
else:
mock_check_output.return_value = b''
output = shell.check_output('ls')
# We always expect str (utf-8) output
self.assertIsInstance(output, six.text_type)
if six.PY3:
mock_check_output.assert_called_with('ls', encoding='utf-8')
else:
mock_check_output.assert_called_with('ls')
class TestShellUtilities(unittest.TestCase):
"""Unit tests for the shell utility wrappers defined in the
build_swift.shell module.
"""
# -------------------------------------------------------------------------
# copy
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('shutil.copyfile', MagicMock())
@patch('build_swift.shell._convert_pathlib_path')
def test_copy_converts_pathlib_paths(self, mock_convert):
source = Path('/source/path')
dest = Path('/dest/path')
shell.copy(source, dest)
mock_convert.assert_has_calls([
mock.call(source),
mock.call(dest),
])
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('shutil.copyfile')
def test_copy_files(self, mock_copyfile):
source = '/source/path'
dest = '/dest/path'
shell.copy(source, dest)
mock_copyfile.assert_called_with(source, dest)
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=False))
@patch('os.path.isdir', MagicMock(return_value=True))
@patch('shutil.copytree')
def test_copy_directories(self, mock_copytree):
source = '/source/path'
dest = '/dest/path'
shell.copy(source, dest)
mock_copytree.assert_called_with(source, dest)
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('shutil.copyfile', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_copy_echos_fake_cp_file_command(self, mock_stdout):
source = '/source/path'
dest = '/dest/path'
shell.copy(source, dest, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> cp {} {}\n'.format(source, dest))
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=False))
@patch('os.path.isdir', MagicMock(return_value=True))
@patch('shutil.copytree', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_copy_echos_fake_cp_directory_command(self, mock_stdout):
source = '/source/path'
dest = '/dest/path'
shell.copy(source, dest, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> cp -R {} {}\n'.format(source, dest))
# -------------------------------------------------------------------------
# pushd
@utils.requires_module('unittest.mock')
@utils.requires_module('pathlib')
@patch('os.getcwd', MagicMock(return_value='/start/path'))
@patch('build_swift.shell._convert_pathlib_path')
def test_pushd_converts_pathlib_path(self, mock_convert):
path = Path('/other/path')
mock_convert.return_value = six.text_type(path)
shell.pushd(path)
mock_convert.assert_called_with(path)
@utils.requires_module('unittest.mock')
@patch('os.getcwd', MagicMock(return_value='/start/path'))
@patch('os.chdir')
def test_pushd_restores_cwd(self, mock_chdir):
with shell.pushd('/other/path'):
mock_chdir.assert_called_with('/other/path')
mock_chdir.assert_called_with('/start/path')
@utils.requires_module('unittest.mock')
@patch('os.getcwd', MagicMock(return_value='/start/path'))
@patch('os.chdir', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_pushd_echos_fake_pushd_popd_commands(self, mock_stdout):
with shell.pushd('/other/path', echo=True):
pass
self.assertEqual(mock_stdout.getvalue().splitlines(), [
'>>> pushd /other/path',
'>>> popd'
])
# -------------------------------------------------------------------------
# makedirs
@utils.requires_module('unittest.mock')
@utils.requires_module('pathlib')
@patch('os.path.exists', MagicMock(return_value=False))
@patch('os.makedirs', MagicMock())
@patch('build_swift.shell._convert_pathlib_path')
def test_makedirs_converts_pathlib_path(self, mock_convert):
path = Path('/some/directory')
shell.makedirs(path)
mock_convert.assert_called_with(path)
@utils.requires_module('unittest.mock')
@patch('os.path.exists', MagicMock(return_value=True))
@patch('os.makedirs')
def test_makedirs_noop_path_exists(self, mock_makedirs):
shell.makedirs('/some/directory')
mock_makedirs.assert_not_called()
@utils.requires_module('unittest.mock')
@patch('os.path.exists', MagicMock(return_value=False))
@patch('os.makedirs')
def test_makedirs_creates_path(self, mock_makedirs):
path = '/some/directory'
shell.makedirs(path)
mock_makedirs.assert_called_with(path)
@utils.requires_module('unittest.mock')
@patch('os.path.exists', MagicMock(return_value=False))
@patch('os.makedirs', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_makedirs_echos_fake_mkdir_command(self, mock_stdout):
path = '/some/directory'
shell.makedirs(path, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> mkdir -p {}\n'.format(path))
# -------------------------------------------------------------------------
# move
@utils.requires_module('unittest.mock')
@utils.requires_module('pathlib')
@patch('shutil.move', MagicMock())
@patch('build_swift.shell._convert_pathlib_path')
def test_move_converts_pathlib_paths(self, mock_convert):
source = Path('/source/path')
dest = Path('/dest/path')
shell.move(source, dest)
mock_convert.assert_has_calls([
mock.call(source),
mock.call(dest),
])
@utils.requires_module('unittest.mock')
@patch('shutil.move')
def test_move(self, mock_move):
source = '/source/path'
dest = '/dest/path'
shell.move(source, dest)
mock_move.assert_called_with(source, dest)
@utils.requires_module('unittest.mock')
@patch('shutil.move', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_move_echos_fake_mv_command(self, mock_stdout):
source = '/source/path'
dest = '/dest/path'
shell.move(source, dest, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> mv {} {}\n'.format(source, dest))
# -------------------------------------------------------------------------
# remove
@utils.requires_module('unittest.mock')
@utils.requires_module('pathlib')
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('os.remove', MagicMock())
@patch('build_swift.shell._convert_pathlib_path')
def test_remove_converts_pathlib_paths(self, mock_convert):
path = Path('/path/to/remove')
shell.remove(path)
mock_convert.assert_called_with(path)
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('os.remove')
def test_remove_files(self, mock_remove):
path = '/path/to/remove'
shell.remove(path)
mock_remove.assert_called_with(path)
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=False))
@patch('os.path.isdir', MagicMock(return_value=True))
@patch('shutil.rmtree')
def test_remove_directories(self, mock_rmtree):
path = '/path/to/remove'
shell.remove(path)
mock_rmtree.assert_called_with(path, ignore_errors=True)
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('os.remove', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_remove_echos_fake_rm_file_command(self, mock_stdout):
path = '/path/to/remove'
shell.remove(path, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> rm {}\n'.format(path))
@utils.requires_module('unittest.mock')
@patch('os.path.isfile', MagicMock(return_value=False))
@patch('os.path.isdir', MagicMock(return_value=True))
@patch('shutil.rmtree', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_remove_echos_fake_rm_directory_command(self, mock_stdout):
path = '/path/to/remove'
shell.remove(path, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> rm -rf {}\n'.format(path))
# -------------------------------------------------------------------------
# symlink
@utils.requires_module('unittest.mock')
@utils.requires_module('pathlib')
@patch('os.symlink', MagicMock())
@patch('build_swift.shell._convert_pathlib_path')
def test_symlink_converts_pathlib_paths(self, mock_convert):
source = Path('/source/path')
dest = Path('/dest/path')
shell.symlink(source, dest)
mock_convert.assert_has_calls([
mock.call(source),
mock.call(dest),
])
@utils.requires_module('unittest.mock')
@patch('os.symlink')
def test_symlink(self, mock_symlink):
source = '/source/path'
dest = '/dest/path'
shell.symlink(source, dest)
mock_symlink.assert_called_with(source, dest)
@utils.requires_module('unittest.mock')
@patch('os.symlink', MagicMock())
@patch('sys.stdout', new_callable=StringIO)
def test_symlink_echos_fake_ln_command(self, mock_stdout):
source = '/source/path'
dest = '/dest/path'
shell.symlink(source, dest, echo=True)
self.assertEqual(
mock_stdout.getvalue(),
'>>> ln -s {} {}\n'.format(source, dest))
# -------------------------------------------------------------------------
# which
# NOTE: We currently have a polyfill for the shutil.which function. This
# will be swapped out for the real-deal as soon as we convert to Python 3,
# which should be in the near future. We could also use a backport package
# from pypi, but we rely on the shell module working in scripting that does
# not use a virtual environment at the moment. Until we either adopt
# Python 3 by default _or_ enforce virtual environments for all our scripts
# we are stuck with the polyfill.
def test_which(self):
pass
class TestAbstractWrapper(unittest.TestCase):
"""Unit tests for the AbstractWrapper class defined in the build_swift.shell
module.
"""
def test_cannot_be_instantiated(self):
with self.assertRaises(TypeError):
shell.AbstractWrapper()
class TestCommandWrapper(unittest.TestCase):
"""Unit tests for the CommandWrapper class defined in the build_swift.shell
module.
"""
# -------------------------------------------------------------------------
# wraps
def test_wraps(self):
sudo = shell.wraps('sudo')
self.assertIsInstance(sudo, shell.CommandWrapper)
self.assertEqual(sudo.command, ['sudo'])
# -------------------------------------------------------------------------
@utils.requires_module('pathlib')
def test_command_normalized(self):
wrapper = shell.CommandWrapper(['ls', '-al', Path('/tmp')])
self.assertEqual(wrapper._command, ['ls', '-al', '/tmp'])
def test_command_property(self):
git = shell.CommandWrapper('git')
self.assertEqual(git.command, ['git'])
@utils.requires_module('unittest.mock')
def test_callable(self):
ls = shell.CommandWrapper('ls')
with patch.object(ls, 'check_call') as mock_check_call:
ls('-al')
mock_check_call.assert_called_with('-al')
# -------------------------------------------------------------------------
# Subprocess Wrappers
@utils.requires_module('unittest.mock')
@patch('build_swift.shell.Popen')
def test_Popen(self, mock_popen):
ls = shell.CommandWrapper('ls')
ls.Popen('-al')
mock_popen.assert_called_with(['ls', '-al'])
@utils.requires_module('unittest.mock')
@patch('build_swift.shell.call')
def test_call(self, mock_call):
ls = shell.CommandWrapper('ls')
ls.call('-al')
mock_call.assert_called_with(['ls', '-al'])
@utils.requires_module('unittest.mock')
@patch('build_swift.shell.check_call')
def test_check_call(self, mock_check_call):
ls = shell.CommandWrapper('ls')
ls.check_call('-al')
mock_check_call.assert_called_with(['ls', '-al'])
@utils.requires_module('unittest.mock')
@patch('build_swift.shell.check_output')
def test_check_output(self, mock_check_output):
ls = shell.CommandWrapper('ls')
ls.check_output('-al')
mock_check_output.assert_called_with(['ls', '-al'])
class TestExecutableWrapper(unittest.TestCase):
"""Unit tests for the ExecutableWrapper class defined in the
build_swift.shell module.
"""
def test_raises_without_executable(self):
class MyWrapper(shell.ExecutableWrapper):
pass
with self.assertRaises(AttributeError):
MyWrapper()
def test_raises_complex_executable(self):
class MyWrapper(shell.ExecutableWrapper):
EXECUTABLE = ['xcrun', 'swiftc']
with self.assertRaises(AttributeError):
MyWrapper()
@utils.requires_module('pathlib')
def test_converts_pathlib_path(self):
class MyWrapper(shell.ExecutableWrapper):
EXECUTABLE = Path('/usr/local/bin/xbs')
wrapper = MyWrapper()
self.assertEqual(wrapper.EXECUTABLE, '/usr/local/bin/xbs')
def test_command_property(self):
class MyWrapper(shell.ExecutableWrapper):
EXECUTABLE = 'test'
wrapper = MyWrapper()
self.assertEqual(wrapper.command, ['test'])
@utils.requires_module('unittest.mock')
@patch('build_swift.shell.which')
def test_path_property(self, mock_which):
class MyWrapper(shell.ExecutableWrapper):
EXECUTABLE = 'test'
wrapper = MyWrapper()
wrapper.path
mock_which.assert_called_with('test')