Skip to content

Commit

Permalink
Devops: Add unit tests for calculation and parser plugin
Browse files Browse the repository at this point in the history
Unit tests are added for the `CalcJob` and `Parser` plugins. The
reference data for the parser tests were taken from the data of the
AiiDA archive that was part of the AiiDA common workflow paper:
https://archive.materialscloud.org/record/file?filename=acwf-verification_unaries-verification-PBE-v1_results_wien2k.aiida&record_id=1770

The `pytest` package is used for the tests with `pytest-regressions`
being used to compare results with reference files. The `pre-commit`
extra is renamed to `dev` and these dependencies are added to it. Older
versions are declared because currently only Python 3.7 and 3.8 are
declared as being supported.
  • Loading branch information
sphuber committed Dec 15, 2023
1 parent 492f157 commit 20fa9ea
Show file tree
Hide file tree
Showing 139 changed files with 14,807 additions and 5 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ repos:
tests/.*.in$
)$
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.11.5
hooks:
- id: isort
- repo: https://github.com/ambv/black
rev: 23.7.0
rev: 23.3.0
hooks:
- id: black
language_version: python3.8
language_version: python3.7
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ dependencies = [
]

[project.optional-dependencies]
pre-commit = [
'pre-commit~=3.3',
dev = [
'pgtest~=1.3',
'pre-commit~=2.2',
'pytest-regressions~=2.0',
]

[project.entry-points.'aiida.calculations']
Expand Down
107 changes: 107 additions & 0 deletions tests/calculations/test_scf123_lapw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
"""Tests for the :mod:`aiida_wien2k.calculations.run123_lapw` module."""
# pylint: disable=redefined-outer-name
from __future__ import annotations

import io
import typing as t

import pytest
from aiida.orm import Dict, SinglefileData

from aiida_wien2k.calculations.run123_lapw import Wien2kRun123Lapw


def recursive_merge(left: dict[t.Any, t.Any], right: dict[t.Any, t.Any]) -> None:
"""Recursively merge the ``right`` dictionary into the ``left`` dictionary.
:param left: Base dictionary.
:param right: Dictionary to recurisvely merge on top of ``left`` dictionary.
"""
for key, value in right.items():
if key in left and isinstance(left[key], dict) and isinstance(value, dict):
recursive_merge(left[key], value)
else:
left[key] = value


@pytest.fixture
def generate_inputs(aiida_local_code_factory, generate_structure):
"""Return a dictionary of inputs for the ``Wien2kRun123Lapw`."""

def factory(**kwargs):
parameters = {}
recursive_merge(parameters, kwargs.pop("parameters", {}))

inputs = {
"code": aiida_local_code_factory("wien2k-run123_lapw", "/bin/true"),
"aiida_structure": generate_structure(),
"parameters": Dict(dict=parameters),
"metadata": {"options": {"resources": {"num_machines": 1, "num_mpiprocs_per_machine": 1}}},
}
inputs.update(**kwargs)
return inputs

return factory


def test_default_structure(generate_calc_job, generate_inputs):
"""Test the plugin for default inputs with structure as ``StructureData``."""
inputs = generate_inputs()
_, calc_info = generate_calc_job(Wien2kRun123Lapw, inputs=inputs)

assert calc_info.remote_copy_list == []
assert sorted(calc_info.retrieve_list) == sorted(
[
("case/*.scf0"),
("case/*.scf1"),
("case/*.scf2"),
("case/*.scfm"),
("case/*.scfc"),
("case/*.error*"),
("case/*.dayfile"),
("case/*.klist"),
("case/*.in0"),
("case/case.struct"),
]
)

assert len(calc_info.local_copy_list) == 1
assert calc_info.local_copy_list[0][-1] == "case/case.struct"


def test_default_singlefile(generate_calc_job, generate_inputs):
"""Test the plugin for default inputs with structure as ``SinglefileData``."""
structure = SinglefileData(io.BytesIO(b"content"), filename="structure.wien2k").store()
inputs = generate_inputs()
inputs.pop("aiida_structure")
inputs["wien2k_structure"] = structure
_, calc_info = generate_calc_job(Wien2kRun123Lapw, inputs=inputs)

assert calc_info.remote_copy_list == []
assert sorted(calc_info.retrieve_list) == sorted(
[
("case/*.scf0"),
("case/*.scf1"),
("case/*.scf2"),
("case/*.scfm"),
("case/*.scfc"),
("case/*.error*"),
("case/*.dayfile"),
("case/*.klist"),
("case/*.in0"),
("case/case.struct"),
]
)

assert len(calc_info.local_copy_list) == 1
assert calc_info.local_copy_list[0] == (structure.uuid, "structure.wien2k", "case/case.struct")


def test_parameters(generate_calc_job, generate_inputs):
"""Test the ``parameters`` input."""
parameters = {"-i": "100", "-p": True}
inputs = generate_inputs(parameters=parameters)
_, calc_info = generate_calc_job(Wien2kRun123Lapw, inputs=inputs)

assert calc_info.codes_info[0].cmdline_params == ["-i", "100", "-p"]
138 changes: 138 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name
"""Module with test fixtures."""
from __future__ import annotations

import collections
import pathlib

import pytest
from aiida.common.folders import Folder
from aiida.common.links import LinkType
from aiida.engine.utils import instantiate_process
from aiida.manage.manager import get_manager
from aiida.orm import CalcJobNode, FolderData, StructureData, TrajectoryData
from aiida.plugins import ParserFactory
from ase.build import bulk

pytest_plugins = ["aiida.manage.tests.pytest_fixtures"] # pylint: disable=invalid-name


@pytest.fixture
def filepath_tests() -> pathlib.Path:
"""Return the path to the tests folder."""
return pathlib.Path(__file__).resolve().parent


@pytest.fixture
def generate_calc_job(tmp_path):
"""Return a factory to generate a :class:`aiida.engine.CalcJob` instance with the given inputs.
The fixture will call ``prepare_for_submission`` and return a tuple of the temporary folder that was passed to it,
as well as the ``CalcInfo`` instance that it returned.
"""

def factory(process_class, inputs=None, return_process=False):
"""Create a :class:`aiida.engine.CalcJob` instance with the given inputs."""
manager = get_manager()
runner = manager.get_runner()
process = instantiate_process(runner, process_class, **inputs or {})
calc_info = process.prepare_for_submission(Folder(tmp_path))

if return_process:
return process

return tmp_path, calc_info

return factory


@pytest.fixture
def generate_calc_job_node(filepath_tests, aiida_localhost, tmp_path):
"""Create and return a :class:`aiida.orm.CalcJobNode` instance."""

def flatten_inputs(inputs, prefix=""):
"""Flatten inputs recursively like :meth:`aiida.engine.processes.process::Process._flatten_inputs`."""
flat_inputs = []
for key, value in inputs.items():
if isinstance(value, collections.abc.Mapping):
flat_inputs.extend(flatten_inputs(value, prefix=prefix + key + "__"))
else:
flat_inputs.append((prefix + key, value))
return flat_inputs

def factory(
entry_point: str,
directory: str,
test_name: str,
inputs: dict = None,
retrieve_temporary_list: list[str] | None = None,
):
"""Create and return a :class:`aiida.orm.CalcJobNode` instance."""
node = CalcJobNode(
computer=aiida_localhost, process_type=f"aiida.calculations:{entry_point}"
)

if inputs:
for link_label, input_node in flatten_inputs(inputs):
input_node.store()
node.add_incoming(input_node, link_type=LinkType.INPUT_CALC, link_label=link_label)

node.store()

filepath_retrieved = filepath_tests / "parsers" / "fixtures" / directory / test_name

retrieved = FolderData()
retrieved.put_object_from_tree(filepath_retrieved)
retrieved.add_incoming(node, link_type=LinkType.CREATE, link_label="retrieved")
retrieved.store()

if retrieve_temporary_list:
for pattern in retrieve_temporary_list:
for filename in filepath_retrieved.glob(pattern):
filepath = tmp_path / filename.relative_to(filepath_retrieved)
filepath.write_bytes(filename.read_bytes())

return node, tmp_path

return node

return factory


@pytest.fixture(scope="session")
def generate_parser():
"""Fixture to load a parser class for testing parsers."""

def factory(entry_point_name):
"""Fixture to load a parser class for testing parsers.
:param entry_point_name: entry point name of the parser class
:return: the `Parser` sub class
"""
return ParserFactory(entry_point_name)

return factory


@pytest.fixture
def generate_structure():
"""Return factory to generate a ``StructureData`` instance."""

def factory(formula: str = "Si") -> StructureData:
"""Generate a ``StructureData`` instance."""
atoms = bulk(formula)
return StructureData(ase=atoms)

return factory


@pytest.fixture
def generate_trajectory(generate_structure):
"""Return factory to generate a ``TrajectoryData`` instance."""

def factory(formula: str = "Si") -> TrajectoryData:
"""Generate a ``TrajectoryData`` instance."""
return TrajectoryData(structurelist=[generate_structure(formula=formula)])

return factory
84 changes: 84 additions & 0 deletions tests/parsers/fixtures/scf123/default/_scheduler-stderr.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
NN ENDS
LSTART ENDS
KGEN ENDS
KGEN ENDS
NN ENDS
LSTART ENDS
KGEN ENDS
KGEN ENDS
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
KGEN ENDS
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
LAPW0 END
LAPW1 END
LAPW2 END
CORE END
MIXER END
Empty file.
Loading

0 comments on commit 20fa9ea

Please sign in to comment.