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

Measure the test coverage #441

Merged
merged 11 commits into from
Mar 14, 2023
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ jobs:
run: pip install -e .[dev]

- name: Run pytest
run: pytest -v tests
run: pytest -v tests --cov
env:
TAG: ${{ matrix.tag }}

# code coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
flags: python-${{ matrix.python-version }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[![Documentation Status](https://readthedocs.org/projects/aiidalab-widgets-base/badge/?version=latest)](https://aiidalab-widgets-base.readthedocs.io/en/latest/?badge=latest)
[![codecov](https://codecov.io/gh/aiidalab/aiidalab-widgets-base/branch/master/graph/badge.svg)](https://codecov.io/gh/aiidalab/aiidalab-widgets-base)

# AiiDAlab Widgets

Expand Down
2 changes: 1 addition & 1 deletion aiidalab_widgets_base/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def user_modifications(source_structure): # pylint: disable=unused-argument
structure_node = self.structure_node.store()
self.output.value = f"Stored in AiiDA [{structure_node}]"

def undo(self, _):
def undo(self, _=None):
"""Undo modifications."""
self.structure_set_by_undo = True
if self.history:
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dev =
pgtest==1.3.1
pre-commit==2.10.1
pytest~=6.2
pytest-cov~=4.0
pytest-docker~=1.0
pytest-selenium~=4.0
selenium~=4.7.0
Expand Down
157 changes: 157 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import os
import shutil
from collections.abc import Mapping

import pytest

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


@pytest.fixture
def fixture_localhost(aiida_localhost):
"""Return a localhost `Computer`."""
localhost = aiida_localhost
localhost.set_default_mpiprocs_per_machine(1)
return localhost


@pytest.fixture
def generate_calc_job_node(fixture_localhost):
"""Fixture to generate a mock `CalcJobNode` for testing parsers."""

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, Mapping):
flat_inputs.extend(flatten_inputs(value, prefix=prefix + key + "__"))
else:
flat_inputs.append((prefix + key, value))
return flat_inputs

def _generate_calc_job_node(
entry_point_name="base",
computer=None,
test_name=None,
inputs=None,
attributes=None,
retrieve_temporary=None,
):
"""Fixture to generate a mock `CalcJobNode` for testing parsers.
:param entry_point_name: entry point name of the calculation class
:param computer: a `Computer` instance
:param test_name: relative path of directory with test output files in the `fixtures/{entry_point_name}` folder.
:param inputs: any optional nodes to add as input links to the corrent CalcJobNode
:param attributes: any optional attributes to set on the node
:param retrieve_temporary: optional tuple of an absolute filepath of a temporary directory and a list of
filenames that should be written to this directory, which will serve as the `retrieved_temporary_folder`.
For now this only works with top-level files and does not support files nested in directories.
:return: `CalcJobNode` instance with an attached `FolderData` as the `retrieved` node.
"""
from aiida import orm
from aiida.common import LinkType
from aiida.plugins.entry_point import format_entry_point_string

if computer is None:
computer = fixture_localhost

filepath_folder = None

if test_name is not None:
basepath = os.path.dirname(os.path.abspath(__file__))
filename = os.path.join(
entry_point_name[len("quantumespresso.") :], test_name
)
filepath_folder = os.path.join(basepath, "parsers", "fixtures", filename)
filepath_input = os.path.join(filepath_folder, "aiida.in")

entry_point = format_entry_point_string("aiida.calculations", entry_point_name)

node = orm.CalcJobNode(computer=computer, process_type=entry_point)
node.base.attributes.set("input_filename", "aiida.in")
node.base.attributes.set("output_filename", "aiida.out")
node.base.attributes.set("error_filename", "aiida.err")
node.set_option("resources", {"num_machines": 1, "num_mpiprocs_per_machine": 1})
node.set_option("max_wallclock_seconds", 1800)

if attributes:
node.base.attributes.set_many(attributes)

if filepath_folder:
from aiida_quantumespresso.tools.pwinputparser import PwInputFile
from qe_tools.exceptions import ParsingError

try:
with open(filepath_input, encoding="utf-8") as input_file:
parsed_input = PwInputFile(input_file.read())
except (ParsingError, FileNotFoundError):
pass
else:
inputs["structure"] = parsed_input.get_structuredata()
inputs["parameters"] = orm.Dict(parsed_input.namelists)

if inputs:
metadata = inputs.pop("metadata", {})
options = metadata.get("options", {})

for name, option in options.items():
node.set_option(name, option)

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

node.store()

if retrieve_temporary:
dirpath, filenames = retrieve_temporary
for filename in filenames:
try:
shutil.copy(
os.path.join(filepath_folder, filename),
os.path.join(dirpath, filename),
)
except FileNotFoundError:
pass # To test the absence of files in the retrieve_temporary folder

if filepath_folder:
retrieved = orm.FolderData()
retrieved.base.repository.put_object_from_tree(filepath_folder)

# Remove files that are supposed to be only present in the retrieved temporary folder
if retrieve_temporary:
for filename in filenames:
try:
retrieved.base.repository.delete_object(filename)
except OSError:
pass # To test the absence of files in the retrieve_temporary folder

retrieved.base.links.add_incoming(
node, link_type=LinkType.CREATE, link_label="retrieved"
)
retrieved.store()

remote_folder = orm.RemoteData(computer=computer, remote_path="/tmp")
remote_folder.base.links.add_incoming(
node, link_type=LinkType.CREATE, link_label="remote_folder"
)
remote_folder.store()

return node

return _generate_calc_job_node


@pytest.fixture
def structure_data_object():
"""Return a `StructureData` object."""
from aiida import orm

structure = orm.StructureData(
cell=[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]
)
structure.append_atom(position=(0.0, 0.0, 0.0), symbols="Si")
structure.append_atom(position=(1.0, 1.0, 1.0), symbols="Si")
return structure
144 changes: 0 additions & 144 deletions tests/test_process.py
Original file line number Diff line number Diff line change
@@ -1,149 +1,5 @@
import os
import shutil
from collections.abc import Mapping

import pytest
from aiida import orm

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


@pytest.fixture
def fixture_localhost(aiida_localhost):
"""Return a localhost `Computer`."""
localhost = aiida_localhost
localhost.set_default_mpiprocs_per_machine(1)
return localhost


@pytest.fixture
def generate_calc_job_node(fixture_localhost):
"""Fixture to generate a mock `CalcJobNode` for testing parsers."""

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, Mapping):
flat_inputs.extend(flatten_inputs(value, prefix=prefix + key + "__"))
else:
flat_inputs.append((prefix + key, value))
return flat_inputs

def _generate_calc_job_node(
entry_point_name="base",
computer=None,
test_name=None,
inputs=None,
attributes=None,
retrieve_temporary=None,
):
"""Fixture to generate a mock `CalcJobNode` for testing parsers.
:param entry_point_name: entry point name of the calculation class
:param computer: a `Computer` instance
:param test_name: relative path of directory with test output files in the `fixtures/{entry_point_name}` folder.
:param inputs: any optional nodes to add as input links to the corrent CalcJobNode
:param attributes: any optional attributes to set on the node
:param retrieve_temporary: optional tuple of an absolute filepath of a temporary directory and a list of
filenames that should be written to this directory, which will serve as the `retrieved_temporary_folder`.
For now this only works with top-level files and does not support files nested in directories.
:return: `CalcJobNode` instance with an attached `FolderData` as the `retrieved` node.
"""
from aiida import orm
from aiida.common import LinkType
from aiida.plugins.entry_point import format_entry_point_string

if computer is None:
computer = fixture_localhost

filepath_folder = None

if test_name is not None:
basepath = os.path.dirname(os.path.abspath(__file__))
filename = os.path.join(
entry_point_name[len("quantumespresso.") :], test_name
)
filepath_folder = os.path.join(basepath, "parsers", "fixtures", filename)
filepath_input = os.path.join(filepath_folder, "aiida.in")

entry_point = format_entry_point_string("aiida.calculations", entry_point_name)

node = orm.CalcJobNode(computer=computer, process_type=entry_point)
node.base.attributes.set("input_filename", "aiida.in")
node.base.attributes.set("output_filename", "aiida.out")
node.base.attributes.set("error_filename", "aiida.err")
node.set_option("resources", {"num_machines": 1, "num_mpiprocs_per_machine": 1})
node.set_option("max_wallclock_seconds", 1800)

if attributes:
node.base.attributes.set_many(attributes)

if filepath_folder:
from aiida_quantumespresso.tools.pwinputparser import PwInputFile
from qe_tools.exceptions import ParsingError

try:
with open(filepath_input, encoding="utf-8") as input_file:
parsed_input = PwInputFile(input_file.read())
except (ParsingError, FileNotFoundError):
pass
else:
inputs["structure"] = parsed_input.get_structuredata()
inputs["parameters"] = orm.Dict(parsed_input.namelists)

if inputs:
metadata = inputs.pop("metadata", {})
options = metadata.get("options", {})

for name, option in options.items():
node.set_option(name, option)

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

node.store()

if retrieve_temporary:
dirpath, filenames = retrieve_temporary
for filename in filenames:
try:
shutil.copy(
os.path.join(filepath_folder, filename),
os.path.join(dirpath, filename),
)
except FileNotFoundError:
pass # To test the absence of files in the retrieve_temporary folder

if filepath_folder:
retrieved = orm.FolderData()
retrieved.base.repository.put_object_from_tree(filepath_folder)

# Remove files that are supposed to be only present in the retrieved temporary folder
if retrieve_temporary:
for filename in filenames:
try:
retrieved.base.repository.delete_object(filename)
except OSError:
pass # To test the absence of files in the retrieve_temporary folder

retrieved.base.links.add_incoming(
node, link_type=LinkType.CREATE, link_label="retrieved"
)
retrieved.store()

remote_folder = orm.RemoteData(computer=computer, remote_path="/tmp")
remote_folder.base.links.add_incoming(
node, link_type=LinkType.CREATE, link_label="remote_folder"
)
remote_folder.store()

return node

return _generate_calc_job_node


def test_process_inputs(generate_calc_job_node):
"""Test ProcessInputWidget with a simple `CalcJobNode`"""
Expand Down
Loading