diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 5f9fb3974..7546c2acb 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -137,6 +137,13 @@ def validate_filepath_executable(self): with override_log_level(): # Temporarily suppress noisy logging with self.computer.get_transport() as transport: file_exists = transport.isfile(str(self.filepath_executable)) + if file_exists: + mode = transport.get_mode(str(self.filepath_executable)) + # `format(mode, 'b')` with default permissions + # gives 110110100, representing rw-rw-r-- + # Check on index 2 if user has execute + user_has_execute = format(mode, 'b')[2] == '1' + except Exception as exception: raise exceptions.ValidationError( 'Could not connect to the configured computer to determine whether the specified executable exists.' @@ -147,6 +154,13 @@ def validate_filepath_executable(self): f'The provided remote absolute path `{self.filepath_executable}` does not exist on the computer.' ) + if not user_has_execute: + execute_msg = ( + f'The file at the remote absolute path `{self.filepath_executable}` exists, ' + 'but might not actually be executable. Check the permissions.' + ) + raise exceptions.ValidationError(execute_msg) + def can_run_on_computer(self, computer: Computer) -> bool: """Return whether the code can run on a given computer. diff --git a/tests/orm/data/code/test_installed.py b/tests/orm/data/code/test_installed.py index 928d71808..15c297f43 100644 --- a/tests/orm/data/code/test_installed.py +++ b/tests/orm/data/code/test_installed.py @@ -101,10 +101,15 @@ def computer(request, aiida_computer_local, aiida_computer_ssh): @pytest.mark.usefixtures('aiida_profile_clean') @pytest.mark.parametrize('computer', ('core.local', 'core.ssh'), indirect=True) -def test_validate_filepath_executable(ssh_key, computer, bash_path): +def test_validate_filepath_executable(ssh_key, computer, bash_path, tmp_path): """Test the :meth:`aiida.orm.nodes.data.code.installed.InstalledCode.validate_filepath_executable` method.""" + filepath_executable = '/usr/bin/not-existing' + dummy_executable = tmp_path / 'dummy.sh' + # Default Linux permissions are 664, so file is not executable + dummy_executable.touch() code = InstalledCode(computer=computer, filepath_executable=filepath_executable) + dummy_code = InstalledCode(computer=computer, filepath_executable=str(dummy_executable)) with pytest.raises(ValidationError, match=r'Could not connect to the configured computer.*'): code.validate_filepath_executable() @@ -117,6 +122,13 @@ def test_validate_filepath_executable(ssh_key, computer, bash_path): with pytest.raises(ValidationError, match=r'The provided remote absolute path .* does not exist on the computer\.'): code.validate_filepath_executable() + with pytest.raises( + ValidationError, + match=r'The file at the remote absolute path .* exists, but might not actually be executable\.', + ): + # Didn't put this in the if, using transport.put, as we anyway only connect to localhost via SSH in this test + dummy_code.validate_filepath_executable() + code.filepath_executable = str(bash_path.absolute()) code.validate_filepath_executable()