diff --git a/aiida/orm/nodes/node.py b/aiida/orm/nodes/node.py index 06237738e2..73aed91e09 100644 --- a/aiida/orm/nodes/node.py +++ b/aiida/orm/nodes/node.py @@ -1090,6 +1090,11 @@ def _store_from_cache(self, cache_node, with_transaction): if key != Sealable.SEALED_KEY: self.set_attribute(key, value) + # The erase() removes the current content of the sandbox folder. + # If this was not done, the content of the sandbox folder could + # become mangled when copying over the content of the cache + # source repository folder. + self._repository.erase() self.put_object_from_tree(cache_node._repository._get_base_folder().abspath) # pylint: disable=protected-access self._store(with_transaction=with_transaction, clean=False) diff --git a/aiida/orm/utils/repository.py b/aiida/orm/utils/repository.py index 519c96c616..a8aa5155b2 100644 --- a/aiida/orm/utils/repository.py +++ b/aiida/orm/utils/repository.py @@ -262,7 +262,7 @@ def erase(self, force=False): if not force: self.validate_mutability() - self._repo_folder.erase() + self._get_base_folder().erase() def store(self): """Store the contents of the sandbox folder into the repository folder.""" diff --git a/tests/orm/node/test_node.py b/tests/orm/node/test_node.py index ddfedf594c..228ca7082e 100644 --- a/tests/orm/node/test_node.py +++ b/tests/orm/node/test_node.py @@ -11,11 +11,13 @@ """Tests for the Node ORM class.""" import os +import tempfile from aiida.backends.testbase import AiidaTestCase from aiida.common import exceptions, LinkType from aiida.orm import Data, Node, User, CalculationNode, WorkflowNode, load_node from aiida.orm.utils.links import LinkTriple +from aiida.manage.caching import enable_caching class TestNode(AiidaTestCase): @@ -729,3 +731,25 @@ def test_tab_completable_properties(self): self.assertEqual(workflow.outputs.result_b.pk, output2.pk) with self.assertRaises(exceptions.NotExistent): _ = workflow.outputs.some_label + + +# Clearing the DB is needed because other parts of the tests (not using +# the fixture infrastructure) delete the User. +def test_store_from_cache(clear_database_before_test): # pylint: disable=unused-argument + """ + Regression test for storing a Node with (nested) repository + content with caching. + """ + data = Data() + with tempfile.TemporaryDirectory() as tmpdir: + dir_path = os.path.join(tmpdir, 'directory') + os.makedirs(dir_path) + with open(os.path.join(dir_path, 'file'), 'w') as file: + file.write('content') + data.put_object_from_tree(tmpdir) + + data.store() + with enable_caching(): + clone = data.clone().store() + assert clone.get_cache_source() == data.uuid + assert data.get_hash() == clone.get_hash() diff --git a/tests/orm/utils/test_repository.py b/tests/orm/utils/test_repository.py index 9e600ace19..a490459edb 100644 --- a/tests/orm/utils/test_repository.py +++ b/tests/orm/utils/test_repository.py @@ -14,8 +14,9 @@ import tempfile from aiida.backends.testbase import AiidaTestCase -from aiida.orm import Node +from aiida.orm import Node, Data from aiida.orm.utils.repository import File, FileType +from aiida.common.exceptions import ModificationNotAllowed class TestRepository(AiidaTestCase): @@ -147,3 +148,46 @@ def test_put_object_from_tree(self): key = os.path.join(basepath, 'subdir', 'a.txt') content = self.get_file_content(os.path.join('subdir', 'a.txt')) self.assertEqual(node.get_object_content(key), content) + + def test_erase_unstored(self): + """ + Test that _repository.erase removes the content of an unstored + node. + """ + node = Node() + node.put_object_from_tree(self.tempdir, '') + + self.assertEqual(sorted(node.list_object_names()), ['c.txt', 'subdir']) + self.assertEqual(sorted(node.list_object_names('subdir')), ['a.txt', 'b.txt', 'nested']) + + node._repository.erase() # pylint: disable=protected-access + self.assertEqual(node.list_object_names(), []) + + def test_erase_stored_force(self): + """ + Test that _repository.erase removes the content of an stored + Data node when passing force=True. + """ + node = Data() + node.put_object_from_tree(self.tempdir, '') + node.store() + + self.assertEqual(sorted(node.list_object_names()), ['c.txt', 'subdir']) + self.assertEqual(sorted(node.list_object_names('subdir')), ['a.txt', 'b.txt', 'nested']) + + node._repository.erase(force=True) # pylint: disable=protected-access + self.assertEqual(node.list_object_names(), []) + + def test_erase_stored_raise(self): + """ + Test that trying to erase the repository content of a stored + Data node without the force flag raises. + """ + node = Data() + node.put_object_from_tree(self.tempdir, '') + node.store() + + self.assertEqual(sorted(node.list_object_names()), ['c.txt', 'subdir']) + self.assertEqual(sorted(node.list_object_names('subdir')), ['a.txt', 'b.txt', 'nested']) + + self.assertRaises(ModificationNotAllowed, node._repository.erase) # pylint: disable=protected-access