diff --git a/kiwi/command.py b/kiwi/command.py index 22f039dc..8f4aa948 100644 --- a/kiwi/command.py +++ b/kiwi/command.py @@ -26,7 +26,7 @@ from collections import namedtuple from builtins import bytes # project -from .exceptions import KiwiCommandError +from .exceptions import KiwiCommandError, KiwiCommandNotFound class Command(object): @@ -60,6 +60,10 @@ class Command(object): environment = os.environ if custom_env: environment = custom_env + if not self._command_exists(command[0], environment): + raise KiwiCommandNotFound( + 'Command {} not found in the environment'.format(command[0]) + ) try: process = subprocess.Popen( command, @@ -117,6 +121,10 @@ class Command(object): environment = os.environ if custom_env: environment = custom_env + if not self._command_exists(command[0], environment): + raise KiwiCommandNotFound( + 'Command {} not found in the environment'.format(command[0]) + ) try: process = subprocess.Popen( command, @@ -165,3 +173,24 @@ class Command(object): error_available=error_available(), process=process ) + + @classmethod + def _command_exists(self, command, env): + """ + Tests if the given command is executable for the specified + environment. If the specified environment does not include + 'PATH' variable, the method raises a KiwiCommandError + exception + + :param string command: command to validate + :param list env: custom os.environ + + :return: True if the command is found, False otherwise + :rtype: bool + """ + if 'PATH' not in env: + raise KiwiCommandError('No PATH variable in environment') + return any( + os.access(os.path.join(path, command), os.X_OK) + for path in env['PATH'].split(os.pathsep) + ) diff --git a/kiwi/exceptions.py b/kiwi/exceptions.py index 107f3db3..8719de97 100644 --- a/kiwi/exceptions.py +++ b/kiwi/exceptions.py @@ -155,6 +155,13 @@ class KiwiCommandError(KiwiError): """ +class KiwiCommandNotFound(KiwiCommandError): + """ + Exception raised if any executable command cannot be found in + the evironment PATH variable. + """ + + class KiwiCommandNotLoaded(KiwiError): """ Exception raised if a kiwi command task module could not be diff --git a/kiwi/xml_description.py b/kiwi/xml_description.py index 61970d90..d319695f 100644 --- a/kiwi/xml_description.py +++ b/kiwi/xml_description.py @@ -37,7 +37,7 @@ from .exceptions import ( KiwiDescriptionInvalid, KiwiDataStructureError, KiwiDescriptionConflict, - KiwiCommandError + KiwiCommandNotFound ) @@ -124,11 +124,13 @@ class XMLDescription(object): ['jing', Defaults.get_schema_file(), self.description_xslt_processed.name], raise_on_error=False ) - except KiwiCommandError: - log.info('Failed running jing command') + except KiwiCommandNotFound as e: log.info('A detailed schema validation failure report requires jing to be installed') + log.info( + '%s: %s: %s' % ('jing', type(e).__name__, format(e)) + ) return - log.info('Schema validation failed. Jing report below:') + log.info('Schema validation failed. See jing report:') log.info('--> ' + cmd.output) def _parse(self): diff --git a/test/unit/command_test.py b/test/unit/command_test.py index 6066d03b..ab2f7942 100644 --- a/test/unit/command_test.py +++ b/test/unit/command_test.py @@ -8,36 +8,53 @@ import os from .test_helper import * -from kiwi.exceptions import KiwiCommandError +from kiwi.exceptions import KiwiCommandError, KiwiCommandNotFound from kiwi.command import Command class TestCommand(object): @raises(KiwiCommandError) + @patch('os.access') @patch('subprocess.Popen') - def test_run_raises_error(self, mock_popen): + def test_run_raises_error(self, mock_popen, mock_access): mock_process = mock.Mock() mock_process.communicate = mock.Mock( return_value=[str.encode('stdout'), str.encode('stderr')] ) mock_process.returncode = 1 mock_popen.return_value = mock_process + mock_access.return_value = True Command.run(['command', 'args']) + @raises(KiwiCommandError) + @patch('os.access') @patch('subprocess.Popen') - def test_run_does_not_raise_error(self, mock_popen): + def test_run_failure(self, mock_popen, mock_access): + mock_popen.side_effect = KiwiCommandError('Run failure') + mock_access.return_value = True + Command.run(['command', 'args']) + + @raises(KiwiCommandError) + def test_run_invalid_environment(self): + Command.run(['command', 'args'], 'invalidEnvironment') + + @patch('os.access') + @patch('subprocess.Popen') + def test_run_does_not_raise_error(self, mock_popen, mock_access): 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 + mock_access.return_value = True result = Command.run(['command', 'args'], os.environ, False) assert result.error == '(no output on stderr)' assert result.output == 'stdout' + @patch('os.access') @patch('subprocess.Popen') - def test_run(self, mock_popen): + def test_run(self, mock_popen, mock_access): command_run = namedtuple( 'command', ['output', 'error', 'returncode'] ) @@ -52,22 +69,25 @@ class TestCommand(object): ) mock_process.returncode = 0 mock_popen.return_value = mock_process + mock_access.return_value = True assert Command.run(['command', 'args']) == run_result - @raises(KiwiCommandError) + @raises(KiwiCommandNotFound) def test_run_command_does_not_exist(self): Command.run(['does-not-exist']) - @raises(KiwiCommandError) + @raises(KiwiCommandNotFound) def test_call_command_does_not_exist(self): Command.call(['does-not-exist'], os.environ) + @patch('os.access') @patch('subprocess.Popen') @patch('select.select') - def test_call(self, mock_select, mock_popen): + def test_call(self, mock_select, mock_popen, mock_access): mock_select.return_value = [True, False, False] mock_process = mock.Mock() mock_popen.return_value = mock_process + mock_access.return_value = True command_call = namedtuple( 'command', [ 'output', 'output_available', @@ -81,3 +101,11 @@ class TestCommand(object): assert call.output == mock_process.stdout assert call.error == mock_process.stderr assert call.process == mock_process + + @raises(KiwiCommandError) + @patch('os.access') + @patch('subprocess.Popen') + def test_call_failure(self,mock_popen, mock_access): + mock_popen.side_effect = KiwiCommandError('Call failure') + mock_access.return_value = True + call = Command.call(['command', 'args']) diff --git a/test/unit/xml_description_test.py b/test/unit/xml_description_test.py index 3b50a326..ff8d98ac 100644 --- a/test/unit/xml_description_test.py +++ b/test/unit/xml_description_test.py @@ -11,7 +11,7 @@ from kiwi.exceptions import ( KiwiDescriptionInvalid, KiwiDataStructureError, KiwiDescriptionConflict, - KiwiCommandError + KiwiCommandNotFound ) from kiwi.xml_description import XMLDescription @@ -138,7 +138,7 @@ class TestSchema(object): return_value=False ) mock_relax.return_value = mock_validate - mock_command.side_effect = KiwiCommandError('No jing command') + mock_command.side_effect = KiwiCommandNotFound('No jing command') self.description_from_data.load() @raises(KiwiDataStructureError)