Skip to content
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
11 changes: 10 additions & 1 deletion src/aiida_shell/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from aiida.common import exceptions, lang
from aiida.common.warnings import AiidaDeprecationWarning
from aiida.engine import Process, WorkChain, launch
from aiida.orm import AbstractCode, Computer, Data, ProcessNode, SinglefileData, load_code, load_computer
from aiida.orm import AbstractCode, Computer, Data, Dict, ProcessNode, SinglefileData, load_code, load_computer

from aiida_shell import ShellCode, ShellJob

Expand All @@ -32,6 +32,7 @@ def launch_shell_job( # noqa: PLR0913
metadata: dict[str, t.Any] | None = None,
submit: bool = False,
resolve_command: bool = True,
monitors: dict[str, Dict] | None = None,
) -> tuple[dict[str, Data], ProcessNode]:
"""Launch a :class:`aiida_shell.ShellJob` job for the given command.

Expand All @@ -52,6 +53,8 @@ def launch_shell_job( # noqa: PLR0913
:param resolve_command: Whether to resolve the command to the absolute path of the executable. If set to ``True``,
the ``which`` command is executed on the target computer to attempt and determine the absolute path. Otherwise,
the command is set as the ``filepath_executable`` attribute of the created ``AbstractCode`` instance.
:param monitors: Optional dictionary of ``Dict`` nodes to be used as monitors for the job (see AiiDA
documentation on how to define monitors).
:raises TypeError: If the value specified for ``metadata.options.computer`` is not a ``Computer``.
:raises ValueError: If ``resolve_command=True`` and the absolute path of the command on the computer could not be
determined.
Expand All @@ -69,6 +72,7 @@ def launch_shell_job( # noqa: PLR0913
parser=parser,
metadata=metadata,
resolve_command=resolve_command,
monitors=monitors,
)

if submit:
Expand All @@ -91,6 +95,7 @@ def prepare_shell_job_inputs( # noqa: PLR0913
parser: ParserFunctionType | str | None = None,
metadata: dict[str, t.Any] | None = None,
resolve_command: bool = True,
monitors: dict[str, Dict] | None = None,
) -> dict[str, t.Any]:
"""Prepare inputs for the ShellJob based on the provided parameters.

Expand All @@ -110,6 +115,8 @@ def prepare_shell_job_inputs( # noqa: PLR0913
:param resolve_command: Whether to resolve the command to the absolute path of the executable. If set to ``True``,
the ``which`` command is executed on the target computer to attempt and determine the absolute path. Otherwise,
the command is set as the ``filepath_executable`` attribute of the created ``AbstractCode`` instance.
:param monitors: Optional dictionary of ``Dict`` nodes to be used as monitors for the job (see AiiDA
documentation on how to define monitors).
:raises TypeError: If the value specified for ``metadata.options.computer`` is not a ``Computer``.
:raises ValueError: If ``resolve_command=True`` and the absolute path of the command on the computer could not be
determined.
Expand Down Expand Up @@ -148,6 +155,8 @@ def prepare_shell_job_inputs( # noqa: PLR0913
'parser': parser,
'metadata': metadata or {},
}
if monitors:
inputs['monitors'] = monitors

return inputs

Expand Down
2 changes: 1 addition & 1 deletion src/aiida_shell/parsers/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def parse_default_outputs(self, dirpath: pathlib.Path) -> ExitCode:
except FileNotFoundError:
stderr = ''
else:
stderr = node_stderr.get_content() # type: ignore[assignment]
stderr = node_stderr.get_content(mode='r')
self.out(ShellJob.FILENAME_STDERR, node_stderr)

filename_stdout = self.node.get_option('output_filename') or ShellJob.FILENAME_STDOUT
Expand Down
6 changes: 5 additions & 1 deletion tests/calculations/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,11 @@ def test_output_filename(generate_calc_job, generate_code, file_regression):
def test_filename_stdin(generate_calc_job, generate_code, file_regression):
"""Test the ``metadata.options.filename_stdin`` input."""
inputs = {
'code': generate_code('cat'),
# even if 'cat' would be more natural, we use `diff` to avoid issues
# in this specific test, because the exact path of the `cat` binary
# may differ across systems (Linux vs. MacOS), while `diff` seems
# to be (by default) more consistent (in /usr/bin).
'code': generate_code('diff'),
'arguments': List(['{filename}']),
'nodes': {'filename': SinglefileData.from_string('content')},
'metadata': {'options': {'filename_stdin': 'filename'}},
Expand Down
2 changes: 1 addition & 1 deletion tests/calculations/test_shell/test_filename_stdin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ exec > _scheduler-stdout.txt
exec 2> _scheduler-stderr.txt


'/usr/bin/cat' < 'filename' > 'stdout' 2> 'stderr'
'/usr/bin/diff' < 'filename' > 'stdout' 2> 'stderr'

echo $? > status
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import collections
import pathlib
import shutil
import tempfile
import typing as t
import uuid
Expand Down Expand Up @@ -170,8 +171,10 @@ def factory(label='localhost', hostname='localhost', scheduler_type='core.direct
@pytest.fixture
def generate_code(generate_computer):
"""Return a :class:`aiida_shell.data.code.ShellCode` instance, either already existing or created."""
default_command = shutil.which('true') # /bin/true on Linux, /usr/bin/true on macOS
assert default_command is not None, 'The `true` command must be available on the system for the tests to run.'

def factory(command='/bin/true', computer_label='localhost', label=None, entry_point_name='core.shell'):
def factory(command=default_command, computer_label='localhost', label=None, entry_point_name='core.shell'):
"""Return a :class:`aiida_shell.data.code.ShellCode` instance, either already existing or created."""
label = label or str(uuid.uuid4())
computer = generate_computer(computer_label)
Expand Down
29 changes: 27 additions & 2 deletions tests/test_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from aiida_shell.calculations.shell import ShellJob
from aiida_shell.launch import launch_shell_job, prepare_computer

DATE_COMMAND = shutil.which('date')
assert DATE_COMMAND is not None, 'The `date` command must be available in order to run the tests.'


class ShellWorkChain(WorkChain):
"""Implementation of :class:`aiida.engine.processes.workchains.workchain.WorkChain` that submits a ``ShellJob``."""
Expand Down Expand Up @@ -86,7 +89,7 @@ def test_arguments():
shellfunction runs just before midnight and the comparison ``datetime`` call runs in the next day causing the test
to fail, but that seems extremely unlikely.
"""
arguments = ['--iso-8601']
arguments = ['-I'] # equivalent to --iso-8601, but supported also on MacOS
results, node = launch_shell_job('date', arguments=arguments)

assert node.is_finished_ok
Expand Down Expand Up @@ -273,7 +276,7 @@ def job_function():
@pytest.mark.parametrize(
'resolve_command, executable',
(
(True, '/usr/bin/date'),
(True, DATE_COMMAND),
(False, 'date'),
),
)
Expand Down Expand Up @@ -379,3 +382,25 @@ def test_metadata_computer(generate_computer):
_, node = launch_shell_job('date', metadata={'computer': computer})
assert node.is_finished_ok
assert node.inputs.code.computer.uuid == computer.uuid


def test_monitors():
"""Test the ``monitors`` input."""
from aiida.orm import Dict

# Create simple monitor configurations
dict_one = {'entry_point': 'core.always_kill', 'minimum_poll_interval': 60}
dict_two = {'entry_point': 'core.always_kill', 'minimum_poll_interval': 120}
monitors = {
'monitor_one': Dict(dict_one),
'monitor_two': Dict(dict_two),
}

_, node = launch_shell_job('date', monitors=monitors)
assert node.is_finished_ok
assert 'monitors' in node.inputs
assert 'monitor_one' in node.inputs.monitors
assert 'monitor_two' in node.inputs.monitors

assert node.inputs.monitors.monitor_one.get_dict() == dict_one
assert node.inputs.monitors.monitor_two.get_dict() == dict_two