Skip to content

Commit

Permalink
Deduplicate code for tests of archive migration code (#3924)
Browse files Browse the repository at this point in the history
Each archive migration was being tested on some archives included in this
repo in `tools/fixtures/export/migrate` and the test was always the
same. Simply take an archive, migrate it to the next version with the
appropriate method and check that both metadata and data dictionaries of
the migrated data match that of the reference archive. This same test
was implemented for each migration method, but has now been centralized
in a single test using pytest to parametrize the versions.

The testing on external archives from `aiida-export-migration-testing`
has also been streamlined a bit, by defining a new base class called
`ArchiveMigrationTest` that provides some utility methods.
  • Loading branch information
sphuber authored Apr 14, 2020
1 parent 39d8ea0 commit 9a75173
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 496 deletions.
5 changes: 4 additions & 1 deletion aiida/tools/importexport/migration/v03_to_v04.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def add_extras(data):
data.update({'node_extras': node_extras, 'node_extras_conversion': node_extras_conversion})


def migrate_v3_to_v4(metadata, data, folder, *args): # pylint: disable=unused-argument
def migrate_v3_to_v4(metadata, data, *args):
"""
Migration of export files from v0.3 to v0.4
Expand All @@ -446,6 +446,9 @@ def migrate_v3_to_v4(metadata, data, folder, *args): # pylint: disable=unused-a
verify_metadata_version(metadata, old_version)
update_metadata(metadata, new_version)

# The trajectory data migration requires the folder containing all the repository files of the archive
folder = args[0]

# Apply migrations in correct sequential order
migration_base_data_plugin_type_string(data)
migration_process_type(metadata, data)
Expand Down
32 changes: 32 additions & 0 deletions tests/tools/importexport/migration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,35 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Module with tests for export archive migrations."""
from aiida.backends.testbase import AiidaTestCase
from aiida.tools.importexport.migration.utils import verify_metadata_version
from tests.utils.archives import get_json_files


class ArchiveMigrationTest(AiidaTestCase):
"""Base class to write specific tests for a particular export archive migration."""

@classmethod
def setUpClass(cls, *args, **kwargs):
super().setUpClass(*args, **kwargs)
cls.external_archive = {'filepath': 'archives', 'external_module': 'aiida-export-migration-tests'}
cls.core_archive = {'filepath': 'export/migrate'}
cls.maxDiff = None # pylint: disable=invalid-name

def migrate(self, filename_archive, version_old, version_new, migration_method):
"""Migrate one of the archives from `aiida-export-migration-tests`.
:param filename_archive: the relative file name of the archive
:param version_old: version of the archive
:param version_new: version to migrate to
:param migration_method: the migration method that should convert between version_old and version_new
:return: the migrated metadata and data as a tuple
"""
metadata, data = get_json_files(filename_archive, **self.external_archive)
verify_metadata_version(metadata, version=version_old)

migration_method(metadata, data)
verify_metadata_version(metadata, version=version_new)

return metadata, data
62 changes: 62 additions & 0 deletions tests/tools/importexport/migration/test_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name
"""Test the export archive migrations on the archives included in `tests/fixtures/export/migrate`."""
import copy
import pytest

from aiida import get_version
from aiida.tools.importexport.common import Archive
from aiida.tools.importexport.migration.v01_to_v02 import migrate_v1_to_v2
from aiida.tools.importexport.migration.v02_to_v03 import migrate_v2_to_v3
from aiida.tools.importexport.migration.v03_to_v04 import migrate_v3_to_v4
from aiida.tools.importexport.migration.v04_to_v05 import migrate_v4_to_v5
from aiida.tools.importexport.migration.v05_to_v06 import migrate_v5_to_v6
from aiida.tools.importexport.migration.v06_to_v07 import migrate_v6_to_v7
from aiida.tools.importexport.migration.v07_to_v08 import migrate_v7_to_v8
from aiida.tools.importexport.migration.utils import verify_metadata_version
from tests.utils.archives import get_json_files, get_archive_file


@pytest.fixture
def migration_data(request):
"""For a given tuple of two subsequent versions and corresponding migration method, return metadata and data."""
version_old, version_new, migration_method = request.param

filepath_archive = 'export_v{}_simple.aiida'.format(version_new)
metadata_new, data_new = get_json_files(filepath_archive, filepath='export/migrate')
verify_metadata_version(metadata_new, version=version_new)

filepath_archive = get_archive_file('export_v{}_simple.aiida'.format(version_old), filepath='export/migrate')

with Archive(filepath_archive) as archive:
metadata_old = copy.deepcopy(archive.meta_data)
data_old = copy.deepcopy(archive.data)

migration_method(metadata_old, data_old, archive.folder)
verify_metadata_version(metadata_old, version=version_new)

yield version_old, version_new, metadata_old, metadata_new, data_old, data_new


@pytest.mark.parametrize(
'migration_data',
(('0.1', '0.2', migrate_v1_to_v2), ('0.2', '0.3', migrate_v2_to_v3), ('0.3', '0.4', migrate_v3_to_v4),
('0.4', '0.5', migrate_v4_to_v5), ('0.5', '0.6', migrate_v5_to_v6), ('0.6', '0.7', migrate_v6_to_v7),
('0.7', '0.8', migrate_v7_to_v8)),
indirect=True
)
def test_migrations(migration_data):
"""Test each migration method from the `aiida.tools.importexport.migration` module."""
version_old, version_new, metadata_old, metadata_new, data_old, data_new = migration_data

# Remove AiiDA version, since this may change regardless of the migration function
metadata_old.pop('aiida_version')
metadata_new.pop('aiida_version')

# Assert conversion message in `metadata.json` is correct and then remove it for later assertions
metadata_new.pop('conversion_info')
message = 'Converted from version {} to {} with AiiDA v{}'.format(version_old, version_new, get_version())
assert metadata_old.pop('conversion_info')[-1] == message, 'Conversion message after migration is wrong'

assert metadata_old == metadata_new
assert data_old == data_new
59 changes: 0 additions & 59 deletions tests/tools/importexport/migration/test_v01_to_v02.py

This file was deleted.

73 changes: 7 additions & 66 deletions tests/tools/importexport/migration/test_v02_to_v03.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,79 +7,21 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Test export file migration from export version 0.2 to 0.3"""
# pylint: disable=too-many-branches

from aiida.backends.testbase import AiidaTestCase
from aiida.tools.importexport.migration.utils import verify_metadata_version
"""Test export file migration from export version 0.2 to 0.3"""
from aiida.tools.importexport.migration.v02_to_v03 import migrate_v2_to_v3

from tests.utils.archives import get_json_files
from . import ArchiveMigrationTest


class TestMigrateV02toV03(AiidaTestCase):
"""Test migration of export files from export version 0.2 to 0.3"""

@classmethod
def setUpClass(cls, *args, **kwargs):
super().setUpClass(*args, **kwargs)

# Utility helpers
cls.external_archive = {'filepath': 'archives', 'external_module': 'aiida-export-migration-tests'}
cls.core_archive = {'filepath': 'export/migrate'}

def test_migrate_v2_to_v3(self):
"""Test function migrate_v2_to_v3"""
from aiida import get_version

# Get metadata.json and data.json as dicts from v0.2 file archive
metadata_v2, data_v2 = get_json_files('export_v0.2_simple.aiida', **self.core_archive)
verify_metadata_version(metadata_v2, version='0.2')

# Get metadata.json and data.json as dicts from v0.3 file archive
metadata_v3, data_v3 = get_json_files('export_v0.3_simple.aiida', **self.core_archive)
verify_metadata_version(metadata_v3, version='0.3')

# Migrate to v0.3
migrate_v2_to_v3(metadata_v2, data_v2)
verify_metadata_version(metadata_v2, version='0.3')

# Remove AiiDA version, since this may change irregardless of the migration function
metadata_v2.pop('aiida_version')
metadata_v3.pop('aiida_version')

# Assert conversion message in `metadata.json` is correct and then remove it for later assertions
conversion_message = 'Converted from version 0.2 to 0.3 with AiiDA v{}'.format(get_version())
self.assertEqual(
metadata_v2.pop('conversion_info')[-1],
conversion_message,
msg='The conversion message after migration is wrong'
)
metadata_v3.pop('conversion_info')

# Assert changes were performed correctly
self.maxDiff = None # pylint: disable=invalid-name
self.assertDictEqual(
metadata_v2,
metadata_v3,
msg='After migration, metadata.json should equal intended metadata.json from archives'
)
self.assertDictEqual(
data_v2, data_v3, msg='After migration, data.json should equal intended data.json from archives'
)

def test_migrate_v2_to_v3_complete(self):
"""Test migration for file containing complete v0.2 era possibilities"""

# Get metadata.json and data.json as dicts from v0.2 file archive
metadata, data = get_json_files('export_v0.2.aiida', **self.external_archive)
verify_metadata_version(metadata, version='0.2')
class TestMigrate(ArchiveMigrationTest):
"""Tests specific for this archive migration."""

# Migrate to v0.3
migrate_v2_to_v3(metadata, data)
verify_metadata_version(metadata, version='0.3')
def test_migrate_external(self):
"""Test the migration on the test archive provided by the external test package."""
metadata, data = self.migrate('export_v0.2.aiida', '0.2', '0.3', migrate_v2_to_v3)

self.maxDiff = None # pylint: disable=invalid-name
# Check link types
legal_link_types = {'unspecified', 'createlink', 'returnlink', 'inputlink', 'calllink'}
for link in data['links_uuid']:
Expand Down Expand Up @@ -137,7 +79,6 @@ def test_compare_migration_with_aiida_made(self):
metadata_v3.pop('aiida_version')
self.assertDictEqual(metadata_v2, metadata_v3)

self.maxDiff = None
# Compare 'data.json'
self.assertEqual(len(data_v2), len(data_v3))

Expand Down
81 changes: 5 additions & 76 deletions tests/tools/importexport/migration/test_v03_to_v04.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Test export file migration from export version 0.3 to 0.4"""
# pylint: disable=too-many-locals,too-many-branches,too-many-statements

"""Test export file migration from export version 0.3 to 0.4"""
import tarfile
import zipfile

from aiida.backends.testbase import AiidaTestCase
from aiida.common.exceptions import NotExistent
from aiida.common.folders import SandboxFolder
from aiida.common.json import load as jsonload
Expand All @@ -22,79 +20,13 @@
from aiida.tools.importexport.migration.v03_to_v04 import migrate_v3_to_v4

from tests.utils.archives import get_archive_file, get_json_files
from . import ArchiveMigrationTest


class TestMigrateV03toV04(AiidaTestCase):
"""Test migration of export files from export version 0.3 to 0.4"""

@classmethod
def setUpClass(cls, *args, **kwargs):
super().setUpClass(*args, **kwargs)

# Utility helpers
cls.external_archive = {'filepath': 'archives', 'external_module': 'aiida-export-migration-tests'}
cls.core_archive = {'filepath': 'export/migrate'}

def test_migrate_v3_to_v4(self):
"""Test function migrate_v3_to_v4"""
from aiida import get_version

# Get metadata.json and data.json as dicts from v0.4 file archive
metadata_v4, data_v4 = get_json_files('export_v0.4_simple.aiida', **self.core_archive)
verify_metadata_version(metadata_v4, version='0.4')

# Get metadata.json and data.json as dicts from v0.3 file archive
# Cannot use 'get_json_files' for 'export_v0.3_simple.aiida',
# because we need to pass the SandboxFolder to 'migrate_v3_to_v4'
dirpath_archive = get_archive_file('export_v0.3_simple.aiida', **self.core_archive)

with SandboxFolder(sandbox_in_repo=False) as folder:
if zipfile.is_zipfile(dirpath_archive):
extract_zip(dirpath_archive, folder, silent=True)
elif tarfile.is_tarfile(dirpath_archive):
extract_tar(dirpath_archive, folder, silent=True)
else:
raise ValueError('invalid file format, expected either a zip archive or gzipped tarball')

try:
with open(folder.get_abs_path('data.json'), 'r', encoding='utf8') as fhandle:
data_v3 = jsonload(fhandle)
with open(folder.get_abs_path('metadata.json'), 'r', encoding='utf8') as fhandle:
metadata_v3 = jsonload(fhandle)
except IOError:
raise NotExistent('export archive does not contain the required file {}'.format(fhandle.filename))

verify_metadata_version(metadata_v3, version='0.3')

# Migrate to v0.4
migrate_v3_to_v4(metadata_v3, data_v3, folder)
verify_metadata_version(metadata_v3, version='0.4')

# Remove AiiDA version, since this may change irregardless of the migration function
metadata_v3.pop('aiida_version')
metadata_v4.pop('aiida_version')

# Assert conversion message in `metadata.json` is correct and then remove it for later assertions
self.maxDiff = None # pylint: disable=invalid-name
conversion_message = 'Converted from version 0.3 to 0.4 with AiiDA v{}'.format(get_version())
self.assertEqual(
metadata_v3.pop('conversion_info')[-1],
conversion_message,
msg='The conversion message after migration is wrong'
)
metadata_v4.pop('conversion_info')

# Assert changes were performed correctly
self.assertDictEqual(
metadata_v3,
metadata_v4,
msg='After migration, metadata.json should equal intended metadata.json from archives'
)
self.assertDictEqual(
data_v3, data_v4, msg='After migration, data.json should equal intended data.json from archives'
)
class TestMigrate(ArchiveMigrationTest):
"""Tests specific for this archive migration."""

def test_migrate_v3_to_v4_complete(self):
def test_migrate_external(self):
"""Test migration for file containing complete v0.3 era possibilities"""

# Get metadata.json and data.json as dicts from v0.3 file archive
Expand Down Expand Up @@ -138,7 +70,6 @@ def test_migrate_v3_to_v4_complete(self):
## Following checks are based on the archive-file
## Which means there are more legal entities, they are simply not relevant here.

self.maxDiff = None # pylint: disable=invalid-name
# Check schema-changes
new_node_attrs = {'node_type', 'process_type'}
for change in new_node_attrs:
Expand Down Expand Up @@ -331,13 +262,11 @@ def test_compare_migration_with_aiida_made(self):
metadata_v4, data_v4 = get_json_files('export_v0.4.aiida', **self.external_archive)

# Compare 'metadata.json'
self.maxDiff = None
metadata_v3.pop('conversion_info')
metadata_v3.pop('aiida_version')
metadata_v4.pop('aiida_version')
self.assertDictEqual(metadata_v3, metadata_v4)

self.maxDiff = None
# Compare 'data.json'
self.assertEqual(len(data_v3), len(data_v4))

Expand Down
Loading

0 comments on commit 9a75173

Please sign in to comment.