# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""
Tests for specific subclasses of Data
"""
from aiida.backends.testbase import AiidaTestCase
### Here comparisons are defined #####################################
### Each comparison has to be a function with name
### _comparison_COMPARISONNAME
### and accepting three parameters:
### - testclass: the testing class, with the proper AssertXXX methods
### - the dbdata value, i.e. the value parsed by the test
### - comparisondata, the values specified by the user for comparison;
### they typically contain a 'value', and possibly other keys for
### more advanced keys
[docs]def _comparison_AlmostEqual(testclass, dbdata, comparisondata):
"""
Compare two numbers (or a list of numbers) to check that
they are all almost equal (within a default precision of 7 digits)
"""
value = comparisondata['value']
if isinstance(dbdata, (list, tuple)) and isinstance(value, (list, tuple)):
testclass.assertEqual(len(dbdata), len(value))
for i in range(0, len(dbdata)):
testclass.assertAlmostEqual(dbdata[i], value[i])
else:
testclass.assertAlmostEqual(dbdata, value)
[docs]def _comparison_Equal(testclass, dbdata, comparisondata):
"""
Compare two objects to see if they are equal
"""
testclass.assertEqual(dbdata, comparisondata['value'])
[docs]def _comparison_LengthEqual(testclass, dbdata, comparisondata):
"""
Check if the length of the object is equal to the value specified
"""
testclass.assertEqual(len(dbdata), comparisondata['value'])
### End of comparison definition #####################################
[docs]def output_test(pk, testname, skip_uuids_from_inputs=[]):
"""
This is the function that should be used to create a new test from an
existing calculation.
It is possible to simplify the file removing unwanted nodes.
:param pk: PK of Calculation, used for test
:param testname: the name of this test, used to create a new folder.
The folder name will be of the form test_PLUGIN_TESTNAME,
with PLUGIN substituted by the plugin name, with dots replaced by
underscores. Testname can contain only digits, letters and underscores.
:param skip_uuids_from_inputs: a list of UUIDs of input nodes to be
skipped
"""
import os
import json
from aiida.common.folders import Folder
from aiida.orm import JobCalculation
from aiida.orm.utils import load_node
from aiida.orm.importexport import export_tree
c = load_node(pk, parent_class=JobCalculation)
outfolder = "test_{}_{}".format(
c.get_parser_name().replace('.', '_'),
testname)
if not is_valid_folder_name(outfolder):
raise ValueError("The testname is invalid; it can contain only "
"letters, digits or underscores")
if os.path.exists(outfolder):
raise ValueError("Out folder '{}' already exists".format(outfolder))
inputs = []
for node in c.get_inputs():
if node.uuid not in skip_uuids_from_inputs:
inputs.append(node.dbnode)
folder = Folder(outfolder)
to_export = [c.dbnode] + inputs
try:
to_export.append(c.out.retrieved.dbnode)
except AttributeError:
raise ValueError("No output retrieved node; without it, we cannot "
"test the parser!")
export_tree(to_export, folder=folder,
also_parents=False,
also_calc_outputs=False)
# Create an empty checks file
with open(os.path.join(outfolder, '_aiida_checks.json'), 'w') as f:
json.dump({}, f)
for path, dirlist, filelist in os.walk(outfolder):
if len(dirlist) == 0 and len(filelist) == 0:
with open("{}/.gitignore".format(path), 'w') as f:
f.write("# This is a placeholder file, used to make git "
"store an empty folder")
f.flush()
[docs]def is_valid_folder_name(name):
"""
Return True if the string (that will be the folder name of each subtest)
is a valid name for a test function: it should start with ``test_``, and
contain only letters, digits or underscores.
"""
import string
if not name.startswith('test_'):
return False
# Remove valid characters, see if anything remains
bad_characters = name.translate(None, string.letters + string.digits + '_')
if bad_characters:
return False
return True
[docs]class TestParsers(AiidaTestCase):
"""
This class dynamically finds all tests in a given subfolder, and loads
them as different tests.
"""
# To have both the "default" error message from assertXXX, and the
# msg specified by us
longMessage = True
[docs] def read_test(self, outfolder):
import os
import importlib
import json
from aiida.orm import JobCalculation
from aiida.orm.utils import load_node
from aiida.orm.importexport import import_data
imported = import_data(outfolder,
ignore_unknown_nodes=True, silent=True)
calc = None
for _, pk in imported['aiida.backends.djsite.db.models.DbNode']['new']:
c = load_node(pk)
if issubclass(c.__class__, JobCalculation):
calc = c
break
retrieved = calc.out.retrieved
try:
with open(os.path.join(outfolder, '_aiida_checks.json')) as f:
tests = json.load(f)
except IOError:
raise ValueError("This test does not provide a check file!")
except ValueError:
raise ValueError("This test does provide a check file, but it cannot "
"be JSON-decoded!")
mod_path = 'aiida.backends.tests.parser_tests.{}'.format(
os.path.split(outfolder)[1])
skip_test = False
try:
m = importlib.import_module(mod_path)
skip_test = m.skip_condition()
except Exception:
pass
if skip_test:
raise SkipTestException
return calc, {'retrieved': retrieved}, tests
[docs] @classmethod
def return_base_test(cls, folder):
from inspect import isfunction
def base_test(self):
try:
calc, retrieved_nodes, tests = self.read_test(folder)
except SkipTestException:
return None
Parser = calc.get_parserclass()
if Parser is None:
raise NotImplementedError
else:
parser = Parser(calc)
successful, new_nodes_tuple = parser.parse_with_retrieved(
retrieved_nodes)
self.assertTrue(successful, msg="The parser did not succeed")
parsed_output_nodes = dict(new_nodes_tuple)
# All main keys: name of nodes that should be present
for test_node_name in tests:
try:
test_node = parsed_output_nodes[test_node_name]
except KeyError:
raise AssertionError("Output node '{}' expected but "
"not found".format(test_node_name))
# Each subkey: attribute to check
# attr_test is the name of the attribute
for attr_test in tests[test_node_name]:
try:
dbdata = test_node.get_attr(attr_test)
except AttributeError:
raise AssertionError("Attribute '{}' not found in "
"parsed node '{}'".format(
attr_test,
test_node_name))
# Test data from the JSON
attr_test_listtests = tests[test_node_name][attr_test]
for test_number, attr_test_data in enumerate(
attr_test_listtests, start=1):
try:
comparison = attr_test_data.pop('comparison')
except KeyError as e:
raise ValueError(
"Missing '{}' in the '{}' field "
"in '{}' in "
"the test file".format(e.message,
attr_test,
test_node_name))
try:
comparison_test = globals()[
"_comparison_{}".format(comparison)]
except KeyError:
raise ValueError(
"Unsupported '{}' comparison in "
"the '{}' field in '{}' in "
"the test file".format(comparison,
attr_test,
test_node_name))
if not isfunction(comparison_test):
raise TypeError(
"Internal error: the variable _comparison_{} is not a "
"function!".format(comparison))
try:
comparison_test(testclass=self, dbdata=dbdata,
comparisondata=attr_test_data)
except Exception as e:
# I change both the message and the 'args'
# (apparently, args[0] is used by str(e))
# Probably, a 'better' way should be found to do this!
e.message = "Failed test #{} for {}->{}: {}".format(
test_number, test_node_name, attr_test,
e.message)
if e.args:
e.args = tuple(
["Failed test #{} for {}->{}: {}".format(
test_number,
test_node_name,
attr_test,
e.args[0])]
+ list(e.args[1:]))
raise e
return base_test
[docs]class SkipTestException(Exception):
pass