Pytest Can't find executable in aiida_local_code_factory

I’m developing a plugin for software installed on my local machine (so far works great) but I’m trying to get tests implemented. My starting point is the test file setup created when using the cookiecutter tool.

Namely:

  • conftest.py
  • /tests
    • __init__.py
    • test_calculations.py
    • /input_files
      • {input file}

I’m getting an error when loading the code for my software using aiida_local_code_factory in conftest.py

@pytest.fixture(scope="function")
def aimall_code(aiida_local_code_factory):
    """Get an aimall code."""
    return aiida_local_code_factory(
        executable="/Applications/AIMAll/AIMQB.app/Contents/MacOS/aimqb",
        entry_point="aimall",
        label="aimall2",
    )

When this is called, I’m getting an error “ValueError: The executable “/Applications/AIMAll/AIMQB.app/Contents/MacOS/aimqb” was not found in the $PATH.”

Looking at the aiida_local_code_factory definition, I see it tries to find the executable with:

import shutil
executable_path = shutil.which(executable)
if not executable_path:
   raise ValueError(f'The executable "{executable}" was not found in the $PATH.')

I don’t know why this wouldn’t be working for this case. When I run python3 in a terminal and run the same code, I get the path as expected.

>>> import shutil
>>> shutil.which("/Applications/AIMAll/AIMQB.app/Contents/MacOS/aimqb")
'/Applications/AIMAll/AIMQB.app/Contents/MacOS/aimqb'

I’ve tried just passing ‘aimqb’ as the executable, with ‘/Applications/AIMAll/AIMQB.app/Contents/MacOS’ in my system path. I’ve also tried adding the below before the call to aiida_local_code_factory in contest.py

import sys

def aimall_code(...):
    sys.path.insert('/Applications/AIMAll/AIMQB.app/Contents/MacOS',0)
    return .....

Can anyone help me find what might be going wrong here?

Thanks in advance

Hi @kmlefran , before we try and solve this problem, do you expect your unit tests to actually run the aimqb executable? As in to run complete integration tests? Or are you planning to write more unit-style tests where the executable is not actually invoked? Because in the latter case, you could take any old executable as a placeholder.

I suppose a complete integration test? I was hoping the program would be run. Pretty new to Python development and these are some of the first tests I’m writing. Maybe I can just load the code by label in the test rather than whatever the contest.py is supposed to do? Here’s the test. Build inputs, run calculation, check output type

file = SinglefileData(
        file=os.path.join(TEST_DIR, "input_files", "water_wb97xd_augccpvtz_qtaim.wfx")
    )

    # set up calculation
    inputs = {
        "code": aimall_code,
        "parameters": parameters,
        "file": file,
    }

    result = run(CalculationFactory("aimall"), **inputs)
    computed_atomic_props = result["atomic_properties"].get_dict()
    computed_bcp_props = result["bcp_properties"].get_dict()

    assert computed_atomic_props is Dict
    assert computed_bcp_props is Dict

I see, then it is indeed important that the executable exists. The behavior is really weird, I am not sure why shutil.which would complain in the context of pytest compared to when you run it in a normal shell. I have to admit that I don’t have an idea of where the problem could be. As a temporary workaround, you can simply copy the implementation of the aiida_local_code_factory fixture and remove the check. I only added that check to prevent accidental typos in the executable path to cause weird failures.

On another note, since you are working on a plugin for a new code, I wondered if you are aware of aiida-shell. This is a plugin I wrote to make starting to using new codes with AiiDA very easy. You don’t need to write any plugins and can start prototyping quickly.

I am not familiar with aimqb, but looking at your test invocation and a quick google, I think you could run the calculation with aiida-shell. Just run pip install aiida-shell and then run the following:

from aiida_shell import launch_shell_job
results, node = launch_shell_job(
    'aimqb',
    arguments='-nogui -bim=proaim {wfx}',
    nodes={
        'wfx': 'water_wb97xd_augccpvtz_qtaim.wfx'
    }
)
print(results['stdout'].get_content())

As you can see, you can pass any command line arguments that aimqb understands in the arguments input.
This is a very simple example, but the aiida-shell interface has quite a bit of flexibility, just have a look at the docs.
Would be interested to know if this would be helpful for your use case.

2 Likes

Thanks, I’ll try that! aiida_shell definitely looks like it should work for my case, but the plugin is already written. I would have probably used it if I’d known at the start of creating this. But it’s all written and implemented in a few WorkChains I’m using

This worked for this issue for me. For anyone who may run into something similar, my conftest.py now looks like the below. Note that since the pytest fixture is being used in the same file as it’s defined, provide name='my_aim_code' to the fixture and use that name in the code to use the fixture.

import pytest

from aiida.common import exceptions
from aiida.orm import Computer, InstalledCode, QueryBuilder

pytest_plugins = ["aiida.manage.tests.pytest_fixtures"]

#removed the three lines relating to executable file path, 
#and changed filepath_executable in code dictionary to be assigned executable.
#Therefore in this usage, the executable should probably be the whole file 
#path to the executable

@pytest.fixture(name="my_aim_code")
def aiida_local_code_factory2(aiida_localhost):
"""Get an AiiDA code on localhost. 
Modified from https://aiida.readthedocs.io/projects/aiida-core/en/latest/_modules/aiida/manage/tests/pytest_fixtures.html

    Searches in the PATH for a given executable and creates an AiiDA code with provided entry point.

    Usage::

      def test_1(aiida_local_code_factory):
          code = aiida_local_code_factory('quantumespresso.pw', '/usr/bin/pw.x')
          # use code for testing ...

    :return: A function get_code(entry_point, executable) that returns the `Code` node.
    :rtype: object
    """

    def get_code(
        entry_point, executable, computer=aiida_localhost, label=None, **kwargs
    ):
        """Get local code.

        Sets up code for given entry point on given computer.

        :param entry_point: Entry point of calculation plugin
        :param executable: name of executable; will be searched for in local system PATH.
        :param computer: (local) AiiDA computer
        :param label: Define the label of the code. By default the ``executable`` is taken. This can be useful if
            multiple codes need to be created in a test which require unique labels.
        :param kwargs: Additional keyword arguments that are passed to the code's constructor.
        :return: the `Code` either retrieved from the database or created if it did not yet exist.
        :rtype: :py:class:`~aiida.orm.Code`
        """

        if label is None:
            label = executable

        builder = QueryBuilder().append(
            Computer, filters={"uuid": computer.uuid}, tag="computer"
        )
        builder.append(
            InstalledCode,
            filters={"label": label, "attributes.input_plugin": entry_point},
            with_computer="computer",
        )

        try:
            code = builder.one()[0]
        except (exceptions.MultipleObjectsError, exceptions.NotExistent):
            code = None
        else:
            return code

        code = InstalledCode(
            label=label,
            description=label,
            default_calc_job_plugin=entry_point,
            computer=computer,
            filepath_executable=executable,
            **kwargs,
        )

        return code.store()

    return get_code

@pytest.fixture(scope="function", autouse=True)
def clear_database_auto(clear_database):  # pylint: disable=unused-argument
    """Automatically clear database in between tests."""


@pytest.fixture(scope="function")
def aimall_code(my_aim_code):
    """Get a aimall code."""
    return my_aim_code(
        executable="/Applications/AIMAll/AIMQB.app/Contents/MacOS/aimqb",
        entry_point="aimall",
        label="aimall",
    )

2 Likes