kiwi-el8/test/unit/command_test.py
Dan Čermák 48817a6441
Refactor Command class
Command.run() currently has a bit of a confusing behavior: if raise_on_error is
False and the executable is not found, then a weird CommandT is returned (return
code is -1 and stdout+stderr is None). This makes it possible to hanlde command
not found errors separately, but it makes that needlessly verbose. So instead,
let's just return None in *this* special case.

That in turn uncovered, that in most cases when we set `raise_on_error=True`, we
actually want an error if the command is not present but no error if the command
fails to execute (e.g. because it returns -1 if you run `$cmd --version`). Hence we
introduce the flag `raise_on_command_not_found`, which causes an exception to
be raised if the command is not found. This makes it independent of the
`raise_on_error` flag.

Additionally, we add a small optimization: if command starts with /, then we
assume it's a full path and we omit the call to which (and just check whether it
exists).

Co-authored-by: Marcus Schäfer <marcus.schaefer@gmail.com>
2024-02-19 16:33:24 +01:00

141 lines
5.0 KiB
Python

from unittest.mock import patch
from collections import namedtuple
from pytest import raises
import unittest.mock as mock
import os
import pytest
from kiwi.command import Command
from kiwi.exceptions import (
KiwiCommandError,
KiwiCommandNotFound
)
class TestCommand:
@patch('kiwi.path.Path.which')
@patch('subprocess.Popen')
def test_run_raises_error(self, mock_popen, mock_which):
mock_which.return_value = 'command'
mock_process = mock.Mock()
mock_process.communicate = mock.Mock(
return_value=[str.encode(''), str.encode('')]
)
mock_process.returncode = 1
mock_popen.return_value = mock_process
with raises(KiwiCommandError):
Command.run(['command', 'args'])
@patch('kiwi.path.Path.which')
@patch('subprocess.Popen')
def test_run_failure(self, mock_popen, mock_which):
mock_which.return_value = 'command'
mock_popen.side_effect = OSError('Run failure')
with raises(KiwiCommandError):
Command.run(['command', 'args'])
def test_run_invalid_environment(self):
with raises(KiwiCommandNotFound):
Command.run(['command', 'args'], {'HOME': '/root'})
@patch('kiwi.path.Path.which')
@patch('subprocess.Popen')
def test_run_does_not_raise_error(self, mock_popen, mock_which):
mock_which.return_value = 'command'
mock_process = mock.Mock()
mock_process.communicate = mock.Mock(
return_value=[str.encode('stdout'), str.encode('')]
)
mock_process.returncode = 1
mock_popen.return_value = mock_process
result = Command.run(['command', 'args'], os.environ, False)
assert result.error == ''
assert result.output == 'stdout'
@patch('kiwi.path.Path.which')
def test_run_does_not_raise_error_if_command_not_found(self, mock_which):
mock_which.return_value = None
result = Command.run(['command', 'args'], os.environ, raise_on_command_not_found=False)
assert result is None
@patch('kiwi.path.Path.which')
@patch('os.path.exists')
@patch('subprocess.Popen')
def test_run_does_not_call_which_for_abspaths(self, mock_popen, mock_exists, mock_which):
mock_exists.return_value = True
proc = mock.MagicMock()
proc.communicate.return_value = (str.encode("stdout"), str.encode("stderr"))
proc.returncode = 0
mock_popen.return_value = proc
result = Command.run(['/bin/command', 'args'])
mock_which.assert_not_called()
assert result is not None
@patch('kiwi.path.Path.which')
@patch('os.path.exists')
def test_run_fails_for_non_existent_abspath(self, mock_exists, mock_which):
mock_exists.return_value = False
with pytest.raises(KiwiCommandNotFound) as cmd_not_found_err:
Command.run(['/bin/command', 'args'])
mock_which.assert_not_called()
assert '"/bin/command" not found' in str(cmd_not_found_err.value)
@patch('os.access')
@patch('os.path.exists')
@patch('subprocess.Popen')
def test_run(self, mock_popen, mock_exists, mock_access):
mock_exists.return_value = True
command_run = namedtuple(
'command', ['output', 'error', 'returncode']
)
run_result = command_run(
output='stdout',
error='stderr',
returncode=0
)
mock_process = mock.Mock()
mock_process.communicate = mock.Mock(
return_value=[str.encode('stdout'), str.encode('stderr')]
)
mock_process.returncode = 0
mock_popen.return_value = mock_process
mock_access.return_value = True
assert Command.run(['command', 'args']) == run_result
def test_run_command_does_not_exist(self):
with raises(KiwiCommandNotFound):
Command.run(['does-not-exist'])
def test_call_command_does_not_exist(self):
with raises(KiwiCommandNotFound):
Command.call(['does-not-exist'], os.environ)
@pytest.mark.parametrize("available", (True, False))
@patch('kiwi.path.Path.which')
@patch('subprocess.Popen')
@patch('select.select')
def test_call(self, mock_select, mock_popen, mock_which, available: bool):
mock_which.return_value = 'command'
mock_select.return_value = [True, False, not available]
mock_process = mock.Mock()
mock_popen.return_value = mock_process
call = Command.call(['command', 'args'])
assert call.output_available() == available
assert call.error_available() == available
assert call.output == mock_process.stdout
assert call.error == mock_process.stderr
assert call.process == mock_process
@patch('kiwi.path.Path.which')
@patch('subprocess.Popen')
def test_call_failure(self, mock_popen, mock_which):
mock_which.return_value = 'command'
mock_popen.side_effect = KiwiCommandError('Call failure')
with raises(KiwiCommandError):
Command.call(['command', 'args'])