Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

verdi code setup: validate the uniqueness of label for local codes #5215

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions aiida/cmdline/params/options/commands/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,34 @@ def is_not_on_computer(ctx):


def validate_label_uniqueness(ctx, _, value):
"""Validate the uniqueness of the full label of the code, i.e., `label@computer.label`.
"""Validate the uniqueness of the label of the code.

The exact uniqueness criterion depends on the type of the code, whether it is "local" or "remote". For the former,
the `label` itself should be unique, whereas for the latter it is the full label, i.e., `label@computer.label`.

.. note:: For this to work in the case of the remote code, the computer parameter already needs to have been parsed
In interactive mode, this means that the computer parameter needs to be defined after the label parameter in the
command definition. For non-interactive mode, the parsing order will always be determined by the order the
parameters are specified by the caller and so this validator may get called before the computer is parsed. For
that reason, this validator should also be called in the command itself, to ensure it has both the label and
computer parameter available.

.. note:: For this to work, the computer parameter already needs to have been parsed. In interactive mode, this
means that the computer parameter needs to be defined after the label parameter in the command definition. For
non-interactive mode, the parsing order will always be determined by the order the parameters are specified by
the caller and so this validator may get called before the computer is parsed. For that reason, this validator
should also be called in the command itself, to ensure it has both the label and computer parameter available.
"""
from aiida.common import exceptions
from aiida.orm import load_code

computer = ctx.params.get('computer', None)
on_computer = ctx.params.get('on_computer', None)

if on_computer is False:
try:
load_code(value)
except exceptions.NotExistent:
pass
except exceptions.MultipleObjectsError:
raise click.BadParameter(f'multiple copies of the remote code `{value}` already exist.')
else:
raise click.BadParameter(f'the code `{value}` already exists.')

if computer is not None:
full_label = f'{value}@{computer.label}'
Expand All @@ -44,6 +60,8 @@ def validate_label_uniqueness(ctx, _, value):
load_code(full_label)
except exceptions.NotExistent:
pass
except exceptions.MultipleObjectsError:
raise click.BadParameter(f'multiple copies of the local code `{full_label}` already exist.')
else:
raise click.BadParameter(f'the code `{full_label}` already exists.')

Expand Down
73 changes: 68 additions & 5 deletions tests/cmdline/commands/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
import tempfile
import textwrap

import click
import pytest

from aiida.cmdline.commands import cmd_code
from aiida.common.exceptions import NotExistent
from aiida.cmdline.params.options.commands.code import validate_label_uniqueness
from aiida.common.exceptions import MultipleObjectsError, NotExistent
from aiida.orm import Code, load_code


Expand Down Expand Up @@ -281,10 +283,10 @@ def test_from_config_url(non_interactive_editor, run_cli_command, aiida_localhos

@pytest.mark.usefixtures('clear_database_before_test')
@pytest.mark.parametrize('non_interactive_editor', ('sleep 1; vim -cwq',), indirect=True)
def test_code_setup_duplicate_full_label_interactive(
def test_code_setup_remote_duplicate_full_label_interactive(
run_cli_command, aiida_local_code_factory, aiida_localhost, non_interactive_editor
):
"""Test ``verdi code setup`` in interactive mode when specifying a full label that already exists."""
"""Test ``verdi code setup`` for a remote code in interactive mode specifying an existing full label."""
label = 'some-label'
aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label)
assert isinstance(load_code(label), Code)
Expand All @@ -297,10 +299,10 @@ def test_code_setup_duplicate_full_label_interactive(

@pytest.mark.usefixtures('clear_database_before_test')
@pytest.mark.parametrize('label_first', (True, False))
def test_code_setup_duplicate_full_label_non_interactive(
def test_code_setup_remote_duplicate_full_label_non_interactive(
run_cli_command, aiida_local_code_factory, aiida_localhost, label_first
):
"""Test ``verdi code setup`` in non-interactive mode when specifying a full label that already exists."""
"""Test ``verdi code setup`` for a remote code in non-interactive mode specifying an existing full label."""
label = 'some-label'
aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label)
assert isinstance(load_code(label), Code)
Expand All @@ -314,3 +316,64 @@ def test_code_setup_duplicate_full_label_non_interactive(

result = run_cli_command(cmd_code.setup_code, options, raises=True)
assert f'the code `{label}@{aiida_localhost.label}` already exists.' in result.output


@pytest.mark.usefixtures('clear_database_before_test')
@pytest.mark.parametrize('non_interactive_editor', ('sleep 1; vim -cwq',), indirect=True)
def test_code_setup_local_duplicate_full_label_interactive(
run_cli_command, aiida_local_code_factory, aiida_localhost, non_interactive_editor
):
"""Test ``verdi code setup`` for a local code in interactive mode specifying an existing full label."""
label = 'some-label'
code = Code(local_executable='bash', files=['/bin/bash'])
code.label = label
code.store()
assert isinstance(load_code(label), Code)

label_unique = 'label-unique'
user_input = '\n'.join(['no', label, label_unique, 'd', 'core.arithmetic.add', '/bin', 'bash'])
run_cli_command(cmd_code.setup_code, user_input=user_input)
assert isinstance(load_code(label_unique), Code)


@pytest.mark.usefixtures('clear_database_before_test')
def test_code_setup_local_duplicate_full_label_non_interactive(
run_cli_command, aiida_local_code_factory, aiida_localhost
):
"""Test ``verdi code setup`` for a local code in non-interactive mode specifying an existing full label."""
label = 'some-label'
code = Code(local_executable='bash', files=['/bin/bash'])
code.label = label
code.store()
assert isinstance(load_code(label), Code)

options = [
'-n', '-D', 'd', '-P', 'core.arithmetic.add', '--store-in-db', '--code-folder=/bin', '--code-rel-path=bash',
'--label', label
]

result = run_cli_command(cmd_code.setup_code, options, raises=True)
assert f'the code `{label}` already exists.' in result.output


@pytest.mark.usefixtures('clear_database_before_test')
def test_validate_label_uniqueness(monkeypatch, aiida_localhost):
"""Test the ``validate_label_uniqueness`` validator."""
from aiida import orm

def load_code(*args, **kwargs):
raise MultipleObjectsError()

monkeypatch.setattr(orm, 'load_code', load_code)

ctx = click.Context(cmd_code.setup_code)
ctx.params = {'on_computer': False}

with pytest.raises(click.BadParameter, match=r'multiple copies of the remote code `.*` already exist.'):
validate_label_uniqueness(ctx, None, 'some-code')

ctx = click.Context(cmd_code.setup_code)
ctx.params = {'on_computer': None, 'computer': aiida_localhost}

with pytest.raises(click.BadParameter, match=r'multiple copies of the local code `.*` already exist.'):
validate_label_uniqueness(ctx, None, 'some-code')