Skip to content

Commit

Permalink
Fix bugs in Node._store_from_cache and Node.repository.erase (#3777)
Browse files Browse the repository at this point in the history
The `_store_from_cache` needed to erase the content of its sandbox
folder before copying over the content of the cache source. Currently,
the existing contents are mixed with with the contents to be copied in.

In fixing this, we discovered an issue in `Node.repository.erase`, where
an unstored node would try erasing the (non-existent) folder in the
permanent repository, instead of the `SandboxFolder`.
  • Loading branch information
greschd authored Feb 21, 2020
1 parent b28d3cf commit 5e26bac
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 2 deletions.
5 changes: 5 additions & 0 deletions aiida/orm/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion aiida/orm/utils/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
24 changes: 24 additions & 0 deletions tests/orm/node/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
46 changes: 45 additions & 1 deletion tests/orm/utils/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

0 comments on commit 5e26bac

Please sign in to comment.