Skip to content

Commit 2f5170d

Browse files
authored
Code: add validate_remote_exec_path method to check executable (#5184)
A common problem is that the filepath of the executable for remote codes is mistyped by accident. The user often doesn't realize until they launch a calculation and it mysteriously fails with a non-descript error. They have to look into the output files to find that the executable could not be found. At that point, it is not trivial to correct the mistake because the `Code` cannot be edited nor can it be deleted, without first deleting the calculation that was just run first. Therefore, it would be nice to warn the user at the time of the code creation or storing. However, the check requires opening a connection to the associated computer which carries both significant overhead, and it may not always be available at time of the code creation. Setup scripts for automated environments may want to configure the computers and codes at a time when they cannot be necessarily reached. Therefore, preventing codes from being created in this case is not acceptable. The compromise is to implement the check in `validate_remote_exec_path` which can then freely be called by a user to check if the executable of the remote code is usable. The method is added to the CLI through the addition of the command `verdi code test`. Also here, we decide to not add the check by default to `verdi code setup` as that should be able to function without internet connection and with minimal overhead. The docs are updated to encourage the user to run `verdi code test` before using it in any calculations if they want to make sure it is functioning. In the future, additional checks can be added to this command.
1 parent 632ab72 commit 2f5170d

File tree

6 files changed

+114
-3
lines changed

6 files changed

+114
-3
lines changed

aiida/cmdline/commands/cmd_code.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from aiida.cmdline.params.options.commands import code as options_code
1919
from aiida.cmdline.utils import echo
2020
from aiida.cmdline.utils.decorators import with_dbenv
21+
from aiida.common import exceptions
2122

2223

2324
@verdi.group('code')
@@ -104,6 +105,26 @@ def setup_code(ctx, non_interactive, **kwargs):
104105
echo.echo_success(f'Code<{code.pk}> {code.full_label} created')
105106

106107

108+
@verdi_code.command('test')
109+
@arguments.CODE(callback=set_code_builder)
110+
@with_dbenv()
111+
def code_test(code):
112+
"""Run tests for the given code to check whether it is usable.
113+
114+
For remote codes the following checks are performed:
115+
116+
* Whether the remote executable exists.
117+
118+
"""
119+
if not code.is_local():
120+
try:
121+
code.validate_remote_exec_path()
122+
except exceptions.ValidationError as exception:
123+
echo.echo_critical(f'validation failed: {exception}')
124+
125+
echo.echo_success('all tests succeeded.')
126+
127+
107128
# Defining the ``COMPUTER`` option first guarantees that the user is prompted for the computer first. This is necessary
108129
# because the ``LABEL`` option has a callback that relies on the computer being already set. Execution order is
109130
# guaranteed only for the interactive case, however. For the non-interactive case, the callback is called explicitly

aiida/orm/nodes/data/code.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212

1313
from aiida.common import exceptions
14+
from aiida.common.log import override_log_level
1415

1516
from .data import Data
1617

@@ -293,6 +294,32 @@ def _validate(self):
293294
if not self.get_remote_exec_path():
294295
raise exceptions.ValidationError('You did not specify a remote executable')
295296

297+
def validate_remote_exec_path(self):
298+
"""Validate the ``remote_exec_path`` attribute.
299+
300+
Checks whether the executable exists on the remote computer if a transport can be opened to it. This method
301+
is intentionally not called in ``_validate`` as to allow the creation of ``Code`` instances whose computers can
302+
not yet be connected to and as to not require the overhead of opening transports in storing a new code.
303+
304+
:raises `~aiida.common.exceptions.ValidationError`: if no transport could be opened or if the defined executable
305+
does not exist on the remote computer.
306+
"""
307+
filepath = self.get_remote_exec_path()
308+
309+
try:
310+
with override_log_level(): # Temporarily suppress noisy logging
311+
with self.computer.get_transport() as transport:
312+
file_exists = transport.isfile(filepath)
313+
except Exception: # pylint: disable=broad-except
314+
raise exceptions.ValidationError(
315+
'Could not connect to the configured computer to determine whether the specified executable exists.'
316+
)
317+
318+
if not file_exists:
319+
raise exceptions.ValidationError(
320+
f'the provided remote absolute path `{filepath}` does not exist on the computer.'
321+
)
322+
296323
def set_prepend_text(self, code):
297324
"""
298325
Pass a string of code that will be put in the scheduler script before the

docs/source/howto/run_codes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,13 @@ Use this, for instance, to load modules or set variables that are needed by the
265265
266266
At the end, you receive a confirmation, with the *PK* and the *UUID* of your new code.
267267

268+
.. tip::
269+
270+
The ``verdi code setup`` command performs minimal checks in order to keep it performant and not rely on an internet connection.
271+
If you want additional checks to verify the code is properly configured and usable, run the `verdi code test` command.
272+
For remote codes for example, this will check whether the associated computer can be connected to and whether the specified executable exists.
273+
Look at the command help to see what other checks may be run.
274+
268275
.. admonition:: Using configuration files
269276
:class: tip title-icon-lightbulb
270277

docs/source/reference/command_line.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Below is a list with all available subcommands.
7878
reveal Reveal one or more hidden codes in `verdi code list`.
7979
setup Setup a new code.
8080
show Display detailed information for a code.
81+
test Run tests for the given code to check whether it is usable.
8182
8283
8384
.. _reference:command-line:verdi-computer:

tests/cmdline/commands/test_code.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from aiida.cmdline.commands import cmd_code
2020
from aiida.cmdline.params.options.commands.code import validate_label_uniqueness
2121
from aiida.common.exceptions import MultipleObjectsError, NotExistent
22-
from aiida.orm import Code, load_code
22+
from aiida.orm import Code, Computer, load_code
2323

2424

2525
@pytest.fixture
@@ -303,7 +303,7 @@ def test_code_setup_remote_duplicate_full_label_non_interactive(
303303
run_cli_command, aiida_local_code_factory, aiida_localhost, label_first
304304
):
305305
"""Test ``verdi code setup`` for a remote code in non-interactive mode specifying an existing full label."""
306-
label = 'some-label'
306+
label = f'some-label-{label_first}'
307307
aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label)
308308
assert isinstance(load_code(label), Code)
309309

@@ -318,7 +318,6 @@ def test_code_setup_remote_duplicate_full_label_non_interactive(
318318
assert f'the code `{label}@{aiida_localhost.label}` already exists.' in result.output
319319

320320

321-
@pytest.mark.usefixtures('clear_database_before_test')
322321
@pytest.mark.parametrize('non_interactive_editor', ('sleep 1; vim -cwq',), indirect=True)
323322
def test_code_setup_local_duplicate_full_label_interactive(
324323
run_cli_command, aiida_local_code_factory, aiida_localhost, non_interactive_editor
@@ -377,3 +376,24 @@ def load_code(*args, **kwargs):
377376

378377
with pytest.raises(click.BadParameter, match=r'multiple copies of the local code `.*` already exist.'):
379378
validate_label_uniqueness(ctx, None, 'some-code')
379+
380+
381+
@pytest.mark.usefixtures('clear_database_before_test')
382+
def test_code_test(run_cli_command):
383+
"""Test the ``verdi code test`` command."""
384+
computer = Computer(
385+
label='test-code-computer', transport_type='core.local', hostname='localhost', scheduler_type='core.slurm'
386+
).store()
387+
code = Code(remote_computer_exec=[computer, '/bin/invalid']).store()
388+
389+
result = run_cli_command(cmd_code.code_test, [str(code.pk)], raises=True)
390+
assert 'Could not connect to the configured computer' in result.output
391+
392+
computer.configure()
393+
394+
result = run_cli_command(cmd_code.code_test, [str(code.pk)], raises=True)
395+
assert 'the provided remote absolute path `/bin/invalid` does not exist' in result.output
396+
397+
code = Code(remote_computer_exec=[computer, '/bin/bash']).store()
398+
result = run_cli_command(cmd_code.code_test, [str(code.pk)])
399+
assert 'all tests succeeded.' in result.output

tests/orm/data/test_code.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
###########################################################################
3+
# Copyright (c), The AiiDA team. All rights reserved. #
4+
# This file is part of the AiiDA code. #
5+
# #
6+
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
7+
# For further information on the license, see the LICENSE.txt file #
8+
# For further information please visit http://www.aiida.net #
9+
###########################################################################
10+
# pylint: disable=redefined-outer-name
11+
"""Tests for :class:`aiida.orm.nodes.data.code.Code` class."""
12+
import pytest
13+
14+
from aiida.common.exceptions import ValidationError
15+
from aiida.orm import Code, Computer
16+
17+
18+
@pytest.mark.usefixtures('clear_database_before_test')
19+
def test_validate_remote_exec_path():
20+
"""Test ``Code.validate_remote_exec_path``."""
21+
computer = Computer(
22+
label='test-code-computer', transport_type='core.local', hostname='localhost', scheduler_type='core.slurm'
23+
).store()
24+
code = Code(remote_computer_exec=[computer, '/bin/invalid'])
25+
26+
with pytest.raises(ValidationError, match=r'Could not connect to the configured computer.*'):
27+
code.validate_remote_exec_path()
28+
29+
computer.configure()
30+
31+
with pytest.raises(ValidationError, match=r'the provided remote absolute path `.*` does not exist.*'):
32+
code.validate_remote_exec_path()
33+
34+
code = Code(remote_computer_exec=[computer, '/bin/bash'])
35+
code.validate_remote_exec_path()

0 commit comments

Comments
 (0)