Skip to content

Commit

Permalink
Fix/406/default calc job plugin (#409)
Browse files Browse the repository at this point in the history
After migrate to AiiDA 2.1, we use InstalledCode to replace the legacy Code class to set the code.
The installedCode accepting the `default_calc_job_plugin` inherit from `AbstractCode` class as parameter to initialize code object the input calcjob plugin, not the `input_plugin` for the legacy Code class.

In this PR, we also add the tests for the computational resource widget by testing the setup a code using quick setup.
Meanwhile, the fixtures are ironed up for clarity.

Co-authored-by: Daniel Hollas <[email protected]>
  • Loading branch information
unkcpz and danielhollas authored Dec 9, 2022
1 parent 001f102 commit 1a8a8cf
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 41 deletions.
43 changes: 21 additions & 22 deletions aiidalab_widgets_base/computational_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ComputationalResourcesWidget(ipw.VBox):
codes = traitlets.Dict(allow_none=True)
allow_hidden_codes = traitlets.Bool(False)
allow_disabled_computers = traitlets.Bool(False)
input_plugin = traitlets.Unicode(allow_none=True)
default_calc_job_plugin = traitlets.Unicode(allow_none=True)

def __init__(self, description="Select code:", path_to_root="../", **kwargs):
"""Dropdown for Codes for one input plugin.
Expand Down Expand Up @@ -82,7 +82,7 @@ def __init__(self, description="Select code:", path_to_root="../", **kwargs):

# Setting up codes and computers.
self.comp_resources_database = ComputationalResourcesDatabaseWidget(
input_plugin=self.input_plugin
default_calc_job_plugin=self.default_calc_job_plugin
)

self.ssh_computer_setup = SshComputerSetup()
Expand Down Expand Up @@ -166,7 +166,7 @@ def _get_codes(self):
for c in orm.QueryBuilder()
.append(
orm.Code,
filters={"attributes.input_plugin": self.input_plugin},
filters={"attributes.input_plugin": self.default_calc_job_plugin},
)
.all()
if c[0].computer.is_user_configured(user)
Expand All @@ -188,9 +188,7 @@ def refresh(self, _=None):
with self.hold_trait_notifications():
self.code_select_dropdown.options = self._get_codes()
if not self.code_select_dropdown.options:
self.output.value = (
f"No codes found for input plugin '{self.input_plugin}'."
)
self.output.value = f"No codes found for default calcjob plugin '{self.default_calc_job_plugin}'."
self.code_select_dropdown.disabled = True
else:
self.code_select_dropdown.disabled = False
Expand Down Expand Up @@ -977,13 +975,14 @@ def __init__(self, path_to_root="../", **kwargs):
style=STYLE,
)

# Computer on which the code is installed. Two dlinks are needed to make sure we get a Computer instance.
# Computer on which the code is installed. The value of this widget is
# the UUID of the selected computer.
self.computer = ComputerDropdownWidget(
path_to_root=path_to_root,
)

# Code plugin.
self.input_plugin = ipw.Dropdown(
self.default_calc_job_plugin = ipw.Dropdown(
options=sorted(
(ep.name, ep.name)
for ep in plugins.entry_point.get_entry_points("aiida.calculations")
Expand All @@ -1001,7 +1000,7 @@ def __init__(self, path_to_root="../", **kwargs):
style=STYLE,
)

self.remote_abs_path = ipw.Text(
self.filepath_executable = ipw.Text(
placeholder="/path/to/executable",
description="Absolute path to executable:",
layout=LAYOUT,
Expand Down Expand Up @@ -1033,9 +1032,9 @@ def __init__(self, path_to_root="../", **kwargs):
children = [
self.label,
self.computer,
self.input_plugin,
self.default_calc_job_plugin,
self.description,
self.remote_abs_path,
self.filepath_executable,
self.use_double_quotes,
self.prepend_text,
self.append_text,
Expand All @@ -1044,10 +1043,10 @@ def __init__(self, path_to_root="../", **kwargs):
]
super().__init__(children, **kwargs)

@traitlets.validate("input_plugin")
def _validate_input_plugin(self, proposal):
@traitlets.validate("default_calc_job_plugin")
def _validate_default_calc_job_plugin(self, proposal):
plugin = proposal["value"]
return plugin if plugin in self.input_plugin.options else None
return plugin if plugin in self.default_calc_job_plugin.options else None

def on_setup_code(self, _=None):
"""Setup an AiiDA code."""
Expand All @@ -1060,7 +1059,6 @@ def on_setup_code(self, _=None):

items_to_configure = [
"label",
"computer",
"description",
"default_calc_job_plugin",
"filepath_executable",
Expand All @@ -1071,24 +1069,25 @@ def on_setup_code(self, _=None):

kwargs = {key: getattr(self, key).value for key in items_to_configure}

# set computer from its widget value the UUID of the computer.
computer = orm.load_computer(self.computer.value)

# Checking if the code with this name already exists
qb = orm.QueryBuilder()
qb.append(
orm.Computer, filters={"uuid": kwargs["computer"].uuid}, tag="computer"
)
qb.append(orm.Computer, filters={"uuid": computer.uuid}, tag="computer")
qb.append(
orm.InstalledCode,
with_computer="computer",
filters={"label": kwargs["label"]},
)
if qb.count() > 0:
self.message = (
f"Code {kwargs['label']}@{kwargs['computer'].label} already exists."
f"Code {kwargs['label']}@{computer.label} already exists."
)
return False

try:
code = orm.InstalledCode(**kwargs)
code = orm.InstalledCode(computer=computer, **kwargs)
except (common.exceptions.InputValidationError, KeyError) as exception:
self.message = f"Invalid inputs: {exception}"
return False
Expand All @@ -1114,7 +1113,7 @@ def _reset(self):
self.label.value = ""
self.computer.value = ""
self.description.value = ""
self.remote_abs_path.value = ""
self.filepath_executable.value = ""
self.use_double_quotes.value = False
self.prepend_text.value = ""
self.append_text.value = ""
Expand All @@ -1130,7 +1129,7 @@ def _observe_code_setup(self, _=None):
self._reset()
for key, value in self.code_setup.items():
if hasattr(self, key):
if key == "input_plugin":
if key == "default_calc_job_plugin":
try:
getattr(self, key).label = value
except traitlets.TraitError:
Expand Down
17 changes: 10 additions & 7 deletions aiidalab_widgets_base/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def _update_structure(self, change: dict) -> None:
class ComputationalResourcesDatabaseWidget(ipw.VBox):
"""Extract the setup of a known computer from the AiiDA code registry."""

input_plugin = traitlets.Unicode(allow_none=True)
default_calc_job_plugin = traitlets.Unicode(allow_none=True)
ssh_config = traitlets.Dict()
computer_setup = traitlets.Dict()
code_setup = traitlets.Dict()
Expand Down Expand Up @@ -261,7 +261,10 @@ def clean_up_database(self, database, plugin):
database[domain][computer].keys()
- {"computer-configure", "computer-setup"}
):
if plugin != database[domain][computer][code]["input_plugin"]:
if (
plugin
!= database[domain][computer][code]["default_calc_job_plugin"]
):
del database[domain][computer][code]
# If no codes remained that correspond to the chosen plugin, remove the computer.
if (
Expand All @@ -284,11 +287,11 @@ def clean_up_database(self, database, plugin):

def update(self, _=None):
database = requests.get(
"https://aiidateam.github.io/aiida-code-registry/database_v2.json"
"https://aiidateam.github.io/aiida-code-registry/database_v2_1.json"
).json()
self.database = (
self.clean_up_database(database, self.input_plugin)
if self.input_plugin
self.clean_up_database(database, self.default_calc_job_plugin)
if self.default_calc_job_plugin
else database
)

Expand Down Expand Up @@ -375,6 +378,6 @@ def _code_changed(self, _=None):

self.code_setup = code_setup

@default("input_plugin")
def _default_input_plugin(self):
@default("default_calc_job_plugin")
def _default_calc_job_plugin(self):
return None
2 changes: 1 addition & 1 deletion notebooks/computational_resources.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"metadata": {},
"outputs": [],
"source": [
"resources = awb.ComputationalResourcesWidget(input_plugin='quantumespresso.pw')"
"resources = awb.ComputationalResourcesWidget()"
]
},
{
Expand Down
30 changes: 21 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,32 @@ def screenshot_dir():


@pytest.fixture(scope="session")
def notebook_service(docker_ip, docker_services):
"""Ensure that HTTP service is up and responsive."""
def docker_compose(docker_services):
return docker_services._docker_compose


@pytest.fixture(scope="session")
def aiidalab_exec(docker_compose):
def execute(command, user=None, **kwargs):
workdir = "/home/jovyan/apps/aiidalab-widgets-base"
if user:
command = f"exec --workdir {workdir} -T --user={user} aiidalab {command}"
else:
command = f"exec --workdir {workdir} -T aiidalab {command}"

docker_compose = docker_services._docker_compose
return docker_compose.execute(command, **kwargs)

return execute


@pytest.fixture(scope="session", autouse=True)
def notebook_service(docker_ip, docker_services, aiidalab_exec):
"""Ensure that HTTP service is up and responsive."""
# Directory ~/apps/aiidalab-widgets-base/ is mounted by docker,
# make it writeable for jovyan user, needed for `pip install`
chmod_command = "exec -T -u root aiidalab bash -c 'chmod -R a+rw /home/jovyan/apps/aiidalab-widgets-base'"
docker_compose.execute(chmod_command)

install_command = "bash -c 'pip install .'"
command = f"exec --workdir /home/jovyan/apps/aiidalab-widgets-base -T aiidalab {install_command}"
aiidalab_exec("chmod -R a+rw /home/jovyan/apps/aiidalab-widgets-base", user="root")

docker_compose.execute(command)
aiidalab_exec("pip install -U .")

# `port_for` takes a container port and returns the corresponding host port
port = docker_services.port_for("aiidalab", 8888)
Expand Down
37 changes: 35 additions & 2 deletions tests/test_notebooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,40 @@ def test_eln_import(selenium_driver, screenshot_dir):
driver.get_screenshot_as_file(f"{screenshot_dir}/eln-import.png")


def test_computational_resources(selenium_driver, screenshot_dir):
def test_computational_resources_code_setup(
selenium_driver, aiidalab_exec, screenshot_dir
):
"""Test the quicksetup of the code"""
# check the code pw-7.0 is not in code list
output = aiidalab_exec("verdi code list").decode().strip()
assert "pw-7.0" not in output

driver = selenium_driver("notebooks/computational_resources.ipynb")
driver.find_element(By.XPATH, '//button[text()="Setup new code"]')

# click the "Setup new code" button
driver.find_element(By.XPATH, '//button[text()="Setup new code"]').click()

# Select daint.cscs.ch domain
driver.find_element(By.XPATH, '//option[text()="daint.cscs.ch"]').click()

# Select computer multicore
driver.find_element(By.XPATH, '//option[text()="multicore"]').click()

# select code pw-7.0-multicore
driver.find_element(By.XPATH, '//option[text()="pw-7.0-multicore"]').click()

# fill the SSH username
driver.find_element(
By.XPATH, "//label[text()='SSH username:']/following-sibling::input"
).send_keys("dummyuser")

# click the quick setup
driver.find_element(By.XPATH, '//button[text()="Quick Setup"]').click()
time.sleep(1.0)

# check the new code pw-7.0@daint-mc is in code list
output = aiidalab_exec("verdi code list").decode().strip()
assert "pw-7.0@daint-mc" in output

# take screenshots
driver.get_screenshot_as_file(f"{screenshot_dir}/computational-resources.png")

0 comments on commit 1a8a8cf

Please sign in to comment.