diff --git a/aiida_quantumespresso/calculations/projwfc.py b/aiida_quantumespresso/calculations/projwfc.py index ad5ef8a62..d81412f9a 100644 --- a/aiida_quantumespresso/calculations/projwfc.py +++ b/aiida_quantumespresso/calculations/projwfc.py @@ -55,6 +55,8 @@ def define(cls, spec): spec.output('projections', valid_type=ProjectionData, required=False) spec.output('bands', valid_type=BandsData, required=False) spec.default_output_node = 'output_parameters' + spec.exit_code(301, 'ERROR_NO_RETRIEVED_TEMPORARY_FOLDER', + message='The retrieved temporary folder could not be accessed.') spec.exit_code(303, 'ERROR_OUTPUT_XML_MISSING', message='The retrieved folder did not contain the required XML file.') spec.exit_code(310, 'ERROR_OUTPUT_STDOUT_READ', diff --git a/aiida_quantumespresso/parsers/projwfc.py b/aiida_quantumespresso/parsers/projwfc.py index acf5f805e..1453e18e3 100644 --- a/aiida_quantumespresso/parsers/projwfc.py +++ b/aiida_quantumespresso/parsers/projwfc.py @@ -12,6 +12,8 @@ from aiida_quantumespresso.parsers.parse_raw.base import ( parse_output_base, convert_qe2aiida_structure, convert_qe_to_kpoints ) +from aiida_quantumespresso.utils.mapping import get_logging_container + from .base import Parser @@ -311,9 +313,13 @@ def parse(self, **kwargs): self.out('output_parameters', Dict(dict=parsed_data)) # Parse the XML to obtain the `structure`, `kpoints` and spin-related settings from the parent calculation + self.exit_code_xml = None parsed_xml, logs_xml = self._parse_xml(retrieved_temporary_folder) self.emit_logs(logs_xml) + if self.exit_code_xml: + return self.exit(self.exit_code_xml) + # we create a dictionary the progressively accumulates more info out_info_dict = {} @@ -368,22 +374,26 @@ def _parse_xml(self, retrieved_temporary_folder): from .parse_xml.exceptions import XMLParseError, XMLUnsupportedFormatError from .parse_xml.pw.parse import parse_xml + logs = get_logging_container() + parsed_xml = {} + xml_filepath = Path(retrieved_temporary_folder) / self.node.process_class.xml_path.name if not xml_filepath.exists(): - self.exit(self.exit_codes.ERROR_OUTPUT_XML_MISSING) + self.exit_code_xml = self.exit_codes.ERROR_OUTPUT_XML_MISSING + return parsed_xml, logs try: with xml_filepath.open('r') as handle: parsed_xml, logs = parse_xml(handle, None) except IOError: - self.exit(self.exit_codes.ERROR_OUTPUT_XML_READ) + self.exit_code_xml = self.exit_codes.ERROR_OUTPUT_XML_READ except XMLParseError: - self.exit(self.exit_codes.ERROR_OUTPUT_XML_PARSE) + self.exit_code_xml = self.exit_codes.ERROR_OUTPUT_XML_PARSE except XMLUnsupportedFormatError: - self.exit(self.exit_codes.ERROR_OUTPUT_XML_FORMAT) + self.exit_code_xml = self.exit_codes.ERROR_OUTPUT_XML_FORMAT except Exception: - self.exit(self.exit_codes.ERROR_UNEXPECTED_PARSER_EXCEPTION) + self.exit_code_xml = self.exit_codes.ERROR_UNEXPECTED_PARSER_EXCEPTION return parsed_xml, logs diff --git a/tests/conftest.py b/tests/conftest.py index 2f106d486..8e0d24450 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -259,7 +259,10 @@ def _generate_calc_job_node( if retrieve_temporary: dirpath, filenames = retrieve_temporary for filename in filenames: - shutil.copy(os.path.join(filepath_folder, filename), os.path.join(dirpath, filename)) + 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() @@ -268,7 +271,10 @@ def _generate_calc_job_node( # Remove files that are supposed to be only present in the retrieved temporary folder if retrieve_temporary: for filename in filenames: - retrieved.delete_object(filename) + try: + retrieved.delete_object(filename) + except OSError: + pass # To test the absence of files in the retrieve_temporary folder retrieved.add_incoming(node, link_type=LinkType.CREATE, link_label='retrieved') retrieved.store() diff --git a/tests/parsers/fixtures/projwfc/xml_format/aiida.out b/tests/parsers/fixtures/projwfc/xml_format/aiida.out new file mode 100644 index 000000000..245818cef --- /dev/null +++ b/tests/parsers/fixtures/projwfc/xml_format/aiida.out @@ -0,0 +1,21 @@ + + Program PROJWFC v.6.6 starts on 3Oct2021 at 13:44:39 + + This program is part of the open-source Quantum ESPRESSO suite + for quantum simulation of materials; please cite + "P. Giannozzi et al., J. Phys.:Condens. Matter 21 395502 (2009); + "P. Giannozzi et al., J. Phys.:Condens. Matter 29 465901 (2017); + URL http://www.quantum-espresso.org", + in publications or presentations arising from this work. More details at + http://www.quantum-espresso.org/quote + +...... OUTPUT TRIMMED + + PROJWFC : 0.62s CPU 0.65s WALL + + + This run was terminated on: 13:44:40 3Oct2021 + +=------------------------------------------------------------------------------= + JOB DONE. +=------------------------------------------------------------------------------= diff --git a/tests/parsers/fixtures/projwfc/xml_format/data-file-schema.xml b/tests/parsers/fixtures/projwfc/xml_format/data-file-schema.xml new file mode 100644 index 000000000..8e58bb362 --- /dev/null +++ b/tests/parsers/fixtures/projwfc/xml_format/data-file-schema.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/parsers/fixtures/projwfc/xml_missing/aiida.out b/tests/parsers/fixtures/projwfc/xml_missing/aiida.out new file mode 100644 index 000000000..245818cef --- /dev/null +++ b/tests/parsers/fixtures/projwfc/xml_missing/aiida.out @@ -0,0 +1,21 @@ + + Program PROJWFC v.6.6 starts on 3Oct2021 at 13:44:39 + + This program is part of the open-source Quantum ESPRESSO suite + for quantum simulation of materials; please cite + "P. Giannozzi et al., J. Phys.:Condens. Matter 21 395502 (2009); + "P. Giannozzi et al., J. Phys.:Condens. Matter 29 465901 (2017); + URL http://www.quantum-espresso.org", + in publications or presentations arising from this work. More details at + http://www.quantum-espresso.org/quote + +...... OUTPUT TRIMMED + + PROJWFC : 0.62s CPU 0.65s WALL + + + This run was terminated on: 13:44:40 3Oct2021 + +=------------------------------------------------------------------------------= + JOB DONE. +=------------------------------------------------------------------------------= diff --git a/tests/parsers/fixtures/projwfc/xml_parse/aiida.out b/tests/parsers/fixtures/projwfc/xml_parse/aiida.out new file mode 100644 index 000000000..245818cef --- /dev/null +++ b/tests/parsers/fixtures/projwfc/xml_parse/aiida.out @@ -0,0 +1,21 @@ + + Program PROJWFC v.6.6 starts on 3Oct2021 at 13:44:39 + + This program is part of the open-source Quantum ESPRESSO suite + for quantum simulation of materials; please cite + "P. Giannozzi et al., J. Phys.:Condens. Matter 21 395502 (2009); + "P. Giannozzi et al., J. Phys.:Condens. Matter 29 465901 (2017); + URL http://www.quantum-espresso.org", + in publications or presentations arising from this work. More details at + http://www.quantum-espresso.org/quote + +...... OUTPUT TRIMMED + + PROJWFC : 0.62s CPU 0.65s WALL + + + This run was terminated on: 13:44:40 3Oct2021 + +=------------------------------------------------------------------------------= + JOB DONE. +=------------------------------------------------------------------------------= diff --git a/tests/parsers/fixtures/projwfc/xml_parse/data-file-schema.xml b/tests/parsers/fixtures/projwfc/xml_parse/data-file-schema.xml new file mode 100644 index 000000000..40d38d29a --- /dev/null +++ b/tests/parsers/fixtures/projwfc/xml_parse/data-file-schema.xml @@ -0,0 +1 @@ +I am not a properly formatted XML file! 😭 diff --git a/tests/parsers/test_projwfc.py b/tests/parsers/test_projwfc.py index a3315d618..7450369d4 100644 --- a/tests/parsers/test_projwfc.py +++ b/tests/parsers/test_projwfc.py @@ -2,23 +2,40 @@ # pylint: disable=redefined-outer-name """Tests for the `ProjwfcParser`.""" +import pytest -def test_projwfc_nonpolarised(fixture_localhost, generate_calc_job_node, generate_parser, data_regression, tmpdir): - """Test ``ProjwfcParser`` on the results of a non-polarised ``projwfc.x`` calculation.""" - entry_point_calc_job = 'quantumespresso.projwfc' - entry_point_parser = 'quantumespresso.projwfc' - retrieve_temporary_list = ['data-file-schema.xml'] - attributes = {'retrieve_temporary_list': retrieve_temporary_list} +@pytest.fixture +def generate_projwfc_node(generate_calc_job_node, fixture_localhost, tmpdir): + """Fixture to constructure a ``projwfc.x`` calcjob node for a specified test.""" - node = generate_calc_job_node( - entry_point_name=entry_point_calc_job, - computer=fixture_localhost, - test_name='nonpolarised', - attributes=attributes, - retrieve_temporary=(tmpdir, retrieve_temporary_list) - ) - parser = generate_parser(entry_point_parser) + def _generate_projwfc_node(test_name): + """Generate a mock ``ProjwfcCalculation`` node for testing the parsing. + + :param test_name: The name of the test folder that contains the output files. + """ + entry_point_calc_job = 'quantumespresso.projwfc' + + retrieve_temporary_list = ['data-file-schema.xml'] + attributes = {'retrieve_temporary_list': retrieve_temporary_list} + + node = generate_calc_job_node( + entry_point_name=entry_point_calc_job, + computer=fixture_localhost, + test_name=test_name, + attributes=attributes, + retrieve_temporary=(tmpdir, retrieve_temporary_list) + ) + return node + + return _generate_projwfc_node + + +@pytest.mark.parametrize('test_name', ('nonpolarised', 'noncollinear', 'spinorbit')) +def test_projwfc(generate_projwfc_node, generate_parser, data_regression, tmpdir, test_name): + """Test ``ProjwfcParser`` on the results of a non-polarised ``projwfc.x`` calculation.""" + node = generate_projwfc_node(test_name) + parser = generate_parser('quantumespresso.projwfc') results, calcfunction = parser.parse_from_node(node, store_provenance=False, retrieved_temporary_folder=tmpdir) assert calcfunction.is_finished, calcfunction.exception @@ -35,22 +52,10 @@ def test_projwfc_nonpolarised(fixture_localhost, generate_calc_job_node, generat }) -def test_projwfc_spinpolarised(fixture_localhost, generate_calc_job_node, generate_parser, data_regression, tmpdir): +def test_projwfc_spinpolarised(generate_projwfc_node, generate_parser, data_regression, tmpdir): """Test ``ProjwfcParser`` on the results of a spin-polarised ``projwfc.x`` calculation.""" - entry_point_calc_job = 'quantumespresso.projwfc' - entry_point_parser = 'quantumespresso.projwfc' - - retrieve_temporary_list = ['data-file-schema.xml'] - attributes = {'retrieve_temporary_list': retrieve_temporary_list} - - node = generate_calc_job_node( - entry_point_name=entry_point_calc_job, - computer=fixture_localhost, - test_name='spinpolarised', - attributes=attributes, - retrieve_temporary=(tmpdir, retrieve_temporary_list) - ) - parser = generate_parser(entry_point_parser) + node = generate_projwfc_node('spinpolarised') + parser = generate_parser('quantumespresso.projwfc') results, calcfunction = parser.parse_from_node(node, store_provenance=False, retrieved_temporary_folder=tmpdir) assert calcfunction.is_finished, calcfunction.exception @@ -70,65 +75,30 @@ def test_projwfc_spinpolarised(fixture_localhost, generate_calc_job_node, genera }) -def test_projwfc_noncollinear(fixture_localhost, generate_calc_job_node, generate_parser, data_regression, tmpdir): - """Test ``ProjwfcParser`` on the results of a noncollinear ``projwfc.x`` calculation.""" - entry_point_calc_job = 'quantumespresso.projwfc' - entry_point_parser = 'quantumespresso.projwfc' - - retrieve_temporary_list = ['data-file-schema.xml'] - attributes = {'retrieve_temporary_list': retrieve_temporary_list} - +def test_projwfc_no_retrieved_temporary(generate_calc_job_node, fixture_localhost, generate_parser): + """Test ``ProjwfcParser`` fails when the retrieved temporary folder is missing.""" node = generate_calc_job_node( - entry_point_name=entry_point_calc_job, + entry_point_name='quantumespresso.projwfc', computer=fixture_localhost, - test_name='noncollinear', - attributes=attributes, - retrieve_temporary=(tmpdir, retrieve_temporary_list) + test_name='xml_missing', ) - parser = generate_parser(entry_point_parser) - results, calcfunction = parser.parse_from_node(node, store_provenance=False, retrieved_temporary_folder=tmpdir) - - assert calcfunction.is_finished, calcfunction.exception - assert calcfunction.is_finished_ok, calcfunction.exit_message - - for link_name in ['output_parameters', 'Dos', 'bands', 'projections']: - assert link_name in results, list(results.keys()) - - data_regression.check({ - 'Dos': results['Dos'].attributes, - 'bands': results['bands'].attributes, - 'projections': - {k: v for k, v in results['projections'].attributes.items() if k not in ['reference_bandsdata_uuid']} - }) - - -def test_projwfc_spinorbit(fixture_localhost, generate_calc_job_node, generate_parser, data_regression, tmpdir): - """Test ``ProjwfcParser`` on the results of a spinorbit ``projwfc.x`` calculation.""" - entry_point_calc_job = 'quantumespresso.projwfc' - entry_point_parser = 'quantumespresso.projwfc' - - retrieve_temporary_list = ['data-file-schema.xml'] - attributes = {'retrieve_temporary_list': retrieve_temporary_list} - - node = generate_calc_job_node( - entry_point_name=entry_point_calc_job, - computer=fixture_localhost, - test_name='spinorbit', - attributes=attributes, - retrieve_temporary=(tmpdir, retrieve_temporary_list) - ) - parser = generate_parser(entry_point_parser) - results, calcfunction = parser.parse_from_node(node, store_provenance=False, retrieved_temporary_folder=tmpdir) - - assert calcfunction.is_finished, calcfunction.exception - assert calcfunction.is_finished_ok, calcfunction.exit_message - - for link_name in ['output_parameters', 'Dos', 'bands', 'projections']: - assert link_name in results, list(results.keys()) - - data_regression.check({ - 'Dos': results['Dos'].attributes, - 'bands': results['bands'].attributes, - 'projections': - {k: v for k, v in results['projections'].attributes.items() if k not in ['reference_bandsdata_uuid']} - }) + parser = generate_parser('quantumespresso.projwfc') + _, calcfunction = parser.parse_from_node(node, store_provenance=False) + + assert calcfunction.is_failed, calcfunction.process_state + assert calcfunction.exit_status == node.process_class.exit_codes.ERROR_NO_RETRIEVED_TEMPORARY_FOLDER.status + + +@pytest.mark.parametrize('test_name, exit_status', ( + ['xml_missing', 303], + ['xml_parse', 321], + ['xml_format', 322], +)) +def test_projwfc_xml_failures(generate_projwfc_node, generate_parser, tmpdir, test_name, exit_status): + """Test ``ProjwfcParser`` fails when the XML is missing.""" + node = generate_projwfc_node(test_name) + parser = generate_parser('quantumespresso.projwfc') + _, calcfunction = parser.parse_from_node(node, store_provenance=False, retrieved_temporary_folder=tmpdir) + + assert calcfunction.is_failed, calcfunction.process_state + assert calcfunction.exit_status == exit_status diff --git a/tests/parsers/test_projwfc/test_projwfc_noncollinear.yml b/tests/parsers/test_projwfc/test_projwfc_noncollinear_.yml similarity index 100% rename from tests/parsers/test_projwfc/test_projwfc_noncollinear.yml rename to tests/parsers/test_projwfc/test_projwfc_noncollinear_.yml diff --git a/tests/parsers/test_projwfc/test_projwfc_nonpolarised.yml b/tests/parsers/test_projwfc/test_projwfc_nonpolarised_.yml similarity index 100% rename from tests/parsers/test_projwfc/test_projwfc_nonpolarised.yml rename to tests/parsers/test_projwfc/test_projwfc_nonpolarised_.yml diff --git a/tests/parsers/test_projwfc/test_projwfc_spinorbit.yml b/tests/parsers/test_projwfc/test_projwfc_spinorbit_.yml similarity index 100% rename from tests/parsers/test_projwfc/test_projwfc_spinorbit.yml rename to tests/parsers/test_projwfc/test_projwfc_spinorbit_.yml