diff --git a/aiida/cmdline/commands/__init__.py b/aiida/cmdline/commands/__init__.py index badf6f3e3b..64278f543c 100644 --- a/aiida/cmdline/commands/__init__.py +++ b/aiida/cmdline/commands/__init__.py @@ -32,5 +32,6 @@ cmd_setup, cmd_shell, cmd_status, + cmd_storage, cmd_user, ) diff --git a/aiida/cmdline/commands/cmd_database.py b/aiida/cmdline/commands/cmd_database.py index 663d7e6e02..586f961926 100644 --- a/aiida/cmdline/commands/cmd_database.py +++ b/aiida/cmdline/commands/cmd_database.py @@ -8,95 +8,60 @@ # For further information please visit http://www.aiida.net # ########################################################################### """`verdi database` commands.""" +# pylint: disable=unused-argument import click from aiida.backends.general.migrations.duplicate_uuids import TABLES_UUID_DEDUPLICATION from aiida.cmdline.commands.cmd_verdi import verdi from aiida.cmdline.params import options -from aiida.cmdline.utils import decorators, echo -from aiida.common import exceptions +from aiida.cmdline.utils import decorators @verdi.group('database') def verdi_database(): - """Inspect and manage the database.""" + """Inspect and manage the database. + + .. deprecated:: v2.0.0 + """ @verdi_database.command('version') +@decorators.deprecated_command( + 'This command has been deprecated and no longer has any effect. It will be removed soon from the CLI (in v2.1).\n' + 'The same information is now available through `verdi status`.\n' +) def database_version(): """Show the version of the database. The database version is defined by the tuple of the schema generation and schema revision. - """ - from aiida.manage.manager import get_manager - - manager = get_manager() - manager._load_backend(schema_check=False) # pylint: disable=protected-access - backend_manager = manager.get_backend_manager() - echo.echo('Generation: ', bold=True, nl=False) - echo.echo(backend_manager.get_schema_generation_database()) - echo.echo('Revision: ', bold=True, nl=False) - echo.echo(backend_manager.get_schema_version_backend()) + .. deprecated:: v2.0.0 + """ @verdi_database.command('migrate') @options.FORCE() -def database_migrate(force): - """Migrate the database to the latest schema version.""" - from aiida.engine.daemon.client import get_daemon_client - from aiida.manage.manager import get_manager - - client = get_daemon_client() - if client.is_daemon_running: - echo.echo_critical('Migration aborted, the daemon for the profile is still running.') - - manager = get_manager() - profile = manager.get_profile() - backend = manager._load_backend(schema_check=False) # pylint: disable=protected-access - - if force: - try: - backend.migrate() - except (exceptions.ConfigurationError, exceptions.DatabaseMigrationError) as exception: - echo.echo_critical(str(exception)) - return - - echo.echo_warning('Migrating your database might take a while and is not reversible.') - echo.echo_warning('Before continuing, make sure you have completed the following steps:') - echo.echo_warning('') - echo.echo_warning(' 1. Make sure you have no active calculations and workflows.') - echo.echo_warning(' 2. If you do, revert the code to the previous version and finish running them first.') - echo.echo_warning(' 3. Stop the daemon using `verdi daemon stop`') - echo.echo_warning(' 4. Make a backup of your database and repository') - echo.echo_warning('') - echo.echo_warning('', nl=False) - - expected_answer = 'MIGRATE NOW' - confirm_message = 'If you have completed the steps above and want to migrate profile "{}", type {}'.format( - profile.name, expected_answer - ) - - try: - response = click.prompt(confirm_message) - while response != expected_answer: - response = click.prompt(confirm_message) - except click.Abort: - echo.echo('\n') - echo.echo_critical('Migration aborted, the data has not been affected.') - else: - try: - backend.migrate() - except (exceptions.ConfigurationError, exceptions.DatabaseMigrationError) as exception: - echo.echo_critical(str(exception)) - else: - echo.echo_success('migration completed') +@click.pass_context +@decorators.deprecated_command( + 'This command has been deprecated and will be removed soon (in v3.0). ' + 'Please call `verdi storage migrate` instead.\n' +) +def database_migrate(ctx, force): + """Migrate the database to the latest schema version. + + .. deprecated:: v2.0.0 + """ + from aiida.cmdline.commands.cmd_storage import backend_migrate + ctx.forward(backend_migrate) @verdi_database.group('integrity') def verdi_database_integrity(): - """Check the integrity of the database and fix potential issues.""" + """Check the integrity of the database and fix potential issues. + + .. deprecated:: v2.0.0 + """ @verdi_database_integrity.command('detect-duplicate-uuid') @@ -110,6 +75,10 @@ def verdi_database_integrity(): @click.option( '-a', '--apply-patch', is_flag=True, help='Actually apply the proposed changes instead of performing a dry run.' ) +@decorators.deprecated_command( + 'This command has been deprecated and no longer has any effect. It will be removed soon from the CLI (in v2.1).\n' + 'For remaining available integrity checks, use `verdi storage integrity` instead.\n' +) def detect_duplicate_uuid(table, apply_patch): """Detect and fix entities with duplicate UUIDs. @@ -119,123 +88,45 @@ def detect_duplicate_uuid(table, apply_patch): constraint on UUIDs on the database level. However, this would leave databases created before this patch with duplicate UUIDs in an inconsistent state. This command will run an analysis to detect duplicate UUIDs in a given table and solve it by generating new UUIDs. Note that it will not delete or merge any rows. - """ - from aiida.backends.general.migrations.duplicate_uuids import deduplicate_uuids - from aiida.manage.manager import get_manager - manager = get_manager() - manager._load_backend(schema_check=False) # pylint: disable=protected-access - try: - messages = deduplicate_uuids(table=table, dry_run=not apply_patch) - except Exception as exception: # pylint: disable=broad-except - echo.echo_critical(f'integrity check failed: {str(exception)}') - else: - for message in messages: - echo.echo_report(message) - - if apply_patch: - echo.echo_success('integrity patch completed') - else: - echo.echo_success('dry-run of integrity patch completed') + .. deprecated:: v2.0.0 + """ @verdi_database_integrity.command('detect-invalid-links') @decorators.with_dbenv() +@decorators.deprecated_command( + 'This command has been deprecated and no longer has any effect. It will be removed soon from the CLI (in v2.1).\n' + 'For remaining available integrity checks, use `verdi storage integrity` instead.\n' +) def detect_invalid_links(): - """Scan the database for invalid links.""" - from tabulate import tabulate - - from aiida.manage.database.integrity.sql.links import INVALID_LINK_SELECT_STATEMENTS - from aiida.manage.manager import get_manager + """Scan the database for invalid links. - integrity_violated = False - - backend = get_manager().get_backend() - - for check in INVALID_LINK_SELECT_STATEMENTS: - - result = backend.execute_prepared_statement(check.sql, check.parameters) - - if result: - integrity_violated = True - echo.echo_warning(f'{check.message}:\n') - echo.echo(tabulate(result, headers=check.headers)) - - if not integrity_violated: - echo.echo_success('no integrity violations detected') - else: - echo.echo_critical('one or more integrity violations detected') + .. deprecated:: v2.0.0 + """ @verdi_database_integrity.command('detect-invalid-nodes') @decorators.with_dbenv() +@decorators.deprecated_command( + 'This command has been deprecated and no longer has any effect. It will be removed soon from the CLI (in v2.1).\n' + 'For remaining available integrity checks, use `verdi storage integrity` instead.\n' +) def detect_invalid_nodes(): - """Scan the database for invalid nodes.""" - from tabulate import tabulate - - from aiida.manage.database.integrity.sql.nodes import INVALID_NODE_SELECT_STATEMENTS - from aiida.manage.manager import get_manager - - integrity_violated = False + """Scan the database for invalid nodes. - backend = get_manager().get_backend() - - for check in INVALID_NODE_SELECT_STATEMENTS: - - result = backend.execute_prepared_statement(check.sql, check.parameters) - - if result: - integrity_violated = True - echo.echo_warning(f'{check.message}:\n') - echo.echo(tabulate(result, headers=check.headers)) - - if not integrity_violated: - echo.echo_success('no integrity violations detected') - else: - echo.echo_critical('one or more integrity violations detected') + .. deprecated:: v2.0.0 + """ @verdi_database.command('summary') +@decorators.deprecated_command( + 'This command has been deprecated and no longer has any effect. It will be removed soon from the CLI (in v2.1).\n' + 'Please call `verdi storage info` instead.\n' +) def database_summary(): - """Summarise the entities in the database.""" - from aiida.cmdline import is_verbose - from aiida.orm import Comment, Computer, Group, Log, Node, QueryBuilder, User - data = {} - - # User - query_user = QueryBuilder().append(User, project=['email']) - data['Users'] = {'count': query_user.count()} - if is_verbose(): - data['Users']['emails'] = query_user.distinct().all(flat=True) - - # Computer - query_comp = QueryBuilder().append(Computer, project=['label']) - data['Computers'] = {'count': query_comp.count()} - if is_verbose(): - data['Computers']['labels'] = query_comp.distinct().all(flat=True) - - # Node - count = QueryBuilder().append(Node).count() - data['Nodes'] = {'count': count} - if is_verbose(): - node_types = QueryBuilder().append(Node, project=['node_type']).distinct().all(flat=True) - data['Nodes']['node_types'] = node_types - process_types = QueryBuilder().append(Node, project=['process_type']).distinct().all(flat=True) - data['Nodes']['process_types'] = [p for p in process_types if p] - - # Group - query_group = QueryBuilder().append(Group, project=['type_string']) - data['Groups'] = {'count': query_group.count()} - if is_verbose(): - data['Groups']['type_strings'] = query_group.distinct().all(flat=True) - - # Comment - count = QueryBuilder().append(Comment).count() - data['Comments'] = {'count': count} - - # Log - count = QueryBuilder().append(Log).count() - data['Logs'] = {'count': count} - - echo.echo_dictionary(data, sort_keys=False, fmt='yaml') + """Summarise the entities in the database. + + .. deprecated:: v2.0.0 + """ diff --git a/aiida/cmdline/commands/cmd_status.py b/aiida/cmdline/commands/cmd_status.py index 1180c7cd0c..bc13b4a694 100644 --- a/aiida/cmdline/commands/cmd_status.py +++ b/aiida/cmdline/commands/cmd_status.py @@ -55,7 +55,7 @@ class ServiceStatus(enum.IntEnum): @click.option('--no-rmq', is_flag=True, help='Do not check RabbitMQ status') def verdi_status(print_traceback, no_rmq): """Print status of AiiDA services.""" - # pylint: disable=broad-except,too-many-statements,too-many-branches + # pylint: disable=broad-except,too-many-statements,too-many-branches,too-many-locals, from aiida import __version__ from aiida.cmdline.utils.daemon import delete_stale_pid_file, get_daemon_status from aiida.common.utils import Capturing @@ -68,16 +68,17 @@ def verdi_status(print_traceback, no_rmq): print_status(ServiceStatus.UP, 'config', AIIDA_CONFIG_FOLDER) manager = get_manager() - profile = manager.get_profile() - - if profile is None: - print_status(ServiceStatus.WARNING, 'profile', 'no profile configured yet') - echo.echo_report('Configure a profile by running `verdi quicksetup` or `verdi setup`.') - return try: profile = manager.get_profile() + + if profile is None: + print_status(ServiceStatus.WARNING, 'profile', 'no profile configured yet') + echo.echo_report('Configure a profile by running `verdi quicksetup` or `verdi setup`.') + return + print_status(ServiceStatus.UP, 'profile', profile.name) + except Exception as exc: message = 'Unable to read AiiDA profile' print_status(ServiceStatus.ERROR, 'profile', message, exception=exc, print_traceback=print_traceback) @@ -95,21 +96,35 @@ def verdi_status(print_traceback, no_rmq): print_status(ServiceStatus.UP, 'repository', repository_status) # Getting the postgres status by trying to get a database cursor - database_data = [profile.database_name, profile.database_username, profile.database_hostname, profile.database_port] + backend_manager = manager.get_backend_manager() + dbgen = backend_manager.get_schema_generation_database() + dbver = backend_manager.get_schema_version_backend() + database_data = [ + profile.database_name, dbgen, dbver, profile.database_username, profile.database_hostname, profile.database_port + ] try: with override_log_level(): # temporarily suppress noisy logging backend = manager.get_backend() backend.cursor() + except IncompatibleDatabaseSchema: - message = 'Database schema version is incompatible with the code: run `verdi database migrate`.' + message = f'Database schema {dbgen} / {dbver} (generation/version) is incompatible with the code. ' + message += 'Run `verdi database migrate` to solve this.' print_status(ServiceStatus.DOWN, 'postgres', message) exit_code = ExitCode.CRITICAL + except Exception as exc: - message = 'Unable to connect to database `{}` as {}@{}:{}'.format(*database_data) + message = 'Unable to connect to database `{}` with schema {} / {} (generation/version) as {}@{}:{}'.format( + *database_data + ) print_status(ServiceStatus.DOWN, 'postgres', message, exception=exc, print_traceback=print_traceback) exit_code = ExitCode.CRITICAL + else: - print_status(ServiceStatus.UP, 'postgres', 'Connected to database `{}` as {}@{}:{}'.format(*database_data)) + message = 'Connected to database `{}` with schema {} / {} (generation/version) as {}@{}:{}'.format( + *database_data + ) + print_status(ServiceStatus.UP, 'postgres', message) # Getting the rmq status if not no_rmq: diff --git a/aiida/cmdline/commands/cmd_storage.py b/aiida/cmdline/commands/cmd_storage.py new file mode 100644 index 0000000000..0430c06092 --- /dev/null +++ b/aiida/cmdline/commands/cmd_storage.py @@ -0,0 +1,125 @@ +# -*- 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 # +########################################################################### +"""`verdi storage` commands.""" + +import click + +from aiida.cmdline.commands.cmd_verdi import verdi +from aiida.cmdline.params import options +from aiida.cmdline.utils import echo +from aiida.common import exceptions + + +@verdi.group('storage') +def verdi_storage(): + """Inspect and manage stored data for a profile.""" + + +@verdi_storage.command('migrate') +@options.FORCE() +def storage_migrate(force): + """Migrate the storage to the latest schema version.""" + from aiida.engine.daemon.client import get_daemon_client + from aiida.manage.manager import get_manager + + client = get_daemon_client() + if client.is_daemon_running: + echo.echo_critical('Migration aborted, the daemon for the profile is still running.') + + manager = get_manager() + profile = manager.get_profile() + backend = manager._load_backend(schema_check=False) # pylint: disable=protected-access + + if force: + try: + backend.migrate() + except (exceptions.ConfigurationError, exceptions.DatabaseMigrationError) as exception: + echo.echo_critical(str(exception)) + return + + echo.echo_warning('Migrating your storage might take a while and is not reversible.') + echo.echo_warning('Before continuing, make sure you have completed the following steps:') + echo.echo_warning('') + echo.echo_warning(' 1. Make sure you have no active calculations and workflows.') + echo.echo_warning(' 2. If you do, revert the code to the previous version and finish running them first.') + echo.echo_warning(' 3. Stop the daemon using `verdi daemon stop`') + echo.echo_warning(' 4. Make a backup of your database and repository') + echo.echo_warning('') + echo.echo_warning('', nl=False) + + expected_answer = 'MIGRATE NOW' + confirm_message = 'If you have completed the steps above and want to migrate profile "{}", type {}'.format( + profile.name, expected_answer + ) + + try: + response = click.prompt(confirm_message) + while response != expected_answer: + response = click.prompt(confirm_message) + except click.Abort: + echo.echo('\n') + echo.echo_critical('Migration aborted, the data has not been affected.') + else: + try: + backend.migrate() + except (exceptions.ConfigurationError, exceptions.DatabaseMigrationError) as exception: + echo.echo_critical(str(exception)) + else: + echo.echo_success('migration completed') + + +@verdi_storage.group('integrity') +def storage_integrity(): + """Checks for the integrity of the data storage.""" + + +@verdi_storage.command('info') +@click.option('--statistics', is_flag=True, help='Provides more in-detail statistically relevant data.') +def storage_info(statistics): + """Summarise the contents of the storage.""" + from aiida.orm import Comment, Computer, Group, Log, Node, QueryBuilder, User + data = {} + + # User + query_user = QueryBuilder().append(User, project=['email']) + data['Users'] = {'count': query_user.count()} + if statistics: + data['Users']['emails'] = query_user.distinct().all(flat=True) + + # Computer + query_comp = QueryBuilder().append(Computer, project=['label']) + data['Computers'] = {'count': query_comp.count()} + if statistics: + data['Computers']['labels'] = query_comp.distinct().all(flat=True) + + # Node + count = QueryBuilder().append(Node).count() + data['Nodes'] = {'count': count} + if statistics: + node_types = QueryBuilder().append(Node, project=['node_type']).distinct().all(flat=True) + data['Nodes']['node_types'] = node_types + process_types = QueryBuilder().append(Node, project=['process_type']).distinct().all(flat=True) + data['Nodes']['process_types'] = [p for p in process_types if p] + + # Group + query_group = QueryBuilder().append(Group, project=['type_string']) + data['Groups'] = {'count': query_group.count()} + if statistics: + data['Groups']['type_strings'] = query_group.distinct().all(flat=True) + + # Comment + count = QueryBuilder().append(Comment).count() + data['Comments'] = {'count': count} + + # Log + count = QueryBuilder().append(Log).count() + data['Logs'] = {'count': count} + + echo.echo_dictionary(data, sort_keys=False, fmt='yaml') diff --git a/docs/source/reference/command_line.rst b/docs/source/reference/command_line.rst index 45f53f41e4..d46b82c91f 100644 --- a/docs/source/reference/command_line.rst +++ b/docs/source/reference/command_line.rst @@ -182,6 +182,8 @@ Below is a list with all available subcommands. Inspect and manage the database. + .. deprecated:: v2.0.0 + Options: --help Show this message and exit. @@ -533,6 +535,26 @@ Below is a list with all available subcommands. --help Show this message and exit. +.. _reference:command-line:verdi-storage: + +``verdi storage`` +----------------- + +.. code:: console + + Usage: [OPTIONS] COMMAND [ARGS]... + + Inspect and manage stored data for a profile. + + Options: + --help Show this message and exit. + + Commands: + info Summarise the contents of the storage. + integrity Checks for the integrity of the data storage. + migrate Migrate the storage to the latest schema version. + + .. _reference:command-line:verdi-user: ``verdi user`` diff --git a/tests/cmdline/commands/test_database.py b/tests/cmdline/commands/test_database.py deleted file mode 100644 index 711a47cbd9..0000000000 --- a/tests/cmdline/commands/test_database.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- 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 # -########################################################################### -# pylint: disable=invalid-name,protected-access -"""Tests for `verdi database`.""" -import enum - -from click.testing import CliRunner -import pytest - -from aiida.backends.testbase import AiidaTestCase -from aiida.cmdline.commands import cmd_database -from aiida.common.links import LinkType -from aiida.orm import CalculationNode, Data, WorkflowNode - - -class TestVerdiDatabasaIntegrity(AiidaTestCase): - """Tests for `verdi database integrity`.""" - - @classmethod - def setUpClass(cls, *args, **kwargs): - """Create a basic valid graph that should help detect false positives.""" - super().setUpClass(*args, **kwargs) - data_input = Data().store() - data_output = Data().store() - calculation = CalculationNode() - workflow_parent = WorkflowNode() - workflow_child = WorkflowNode() - - workflow_parent.add_incoming(data_input, link_label='input', link_type=LinkType.INPUT_WORK) - workflow_parent.store() - - workflow_child.add_incoming(data_input, link_label='input', link_type=LinkType.INPUT_WORK) - workflow_child.add_incoming(workflow_parent, link_label='call', link_type=LinkType.CALL_WORK) - workflow_child.store() - - calculation.add_incoming(data_input, link_label='input', link_type=LinkType.INPUT_CALC) - calculation.add_incoming(workflow_child, link_label='input', link_type=LinkType.CALL_CALC) - calculation.store() - - data_output.add_incoming(calculation, link_label='output', link_type=LinkType.CREATE) - data_output.add_incoming(workflow_child, link_label='output', link_type=LinkType.RETURN) - data_output.add_incoming(workflow_parent, link_label='output', link_type=LinkType.RETURN) - - def setUp(self): - self.refurbish_db() - self.cli_runner = CliRunner() - - def test_detect_invalid_links_workflow_create(self): - """Test `verdi database integrity detect-invalid-links` outgoing `create` from `workflow`.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - # Create an invalid link: outgoing `create` from a workflow - data = Data().store().backend_entity - workflow = WorkflowNode().store().backend_entity - - data.add_incoming(workflow, link_type=LinkType.CREATE, link_label='create') - - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - def test_detect_invalid_links_calculation_return(self): - """Test `verdi database integrity detect-invalid-links` outgoing `return` from `calculation`.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - # Create an invalid link: outgoing `return` from a calculation - data = Data().store().backend_entity - calculation = CalculationNode().store().backend_entity - - data.add_incoming(calculation, link_type=LinkType.RETURN, link_label='return') - - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - def test_detect_invalid_links_calculation_call(self): - """Test `verdi database integrity detect-invalid-links` outgoing `call` from `calculation`.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - # Create an invalid link: outgoing `call` from a calculation - worklow = WorkflowNode().store().backend_entity - calculation = CalculationNode().store().backend_entity - - worklow.add_incoming(calculation, link_type=LinkType.CALL_WORK, link_label='call') - - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - def test_detect_invalid_links_create_links(self): - """Test `verdi database integrity detect-invalid-links` when there are multiple incoming `create` links.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - # Create an invalid link: two `create` links - data = Data().store().backend_entity - calculation = CalculationNode().store().backend_entity - - data.add_incoming(calculation, link_type=LinkType.CREATE, link_label='create') - data.add_incoming(calculation, link_type=LinkType.CREATE, link_label='create') - - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - def test_detect_invalid_links_call_links(self): - """Test `verdi database integrity detect-invalid-links` when there are multiple incoming `call` links.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - # Create an invalid link: two `call` links - workflow = WorkflowNode().store().backend_entity - calculation = CalculationNode().store().backend_entity - - calculation.add_incoming(workflow, link_type=LinkType.CALL_CALC, link_label='call') - calculation.add_incoming(workflow, link_type=LinkType.CALL_CALC, link_label='call') - - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - def test_detect_invalid_links_unknown_link_type(self): - """Test `verdi database integrity detect-invalid-links` when link type is invalid.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - class WrongLinkType(enum.Enum): - - WRONG_CREATE = 'wrong_create' - - # Create an invalid link: invalid link type - data = Data().store().backend_entity - calculation = CalculationNode().store().backend_entity - - data.add_incoming(calculation, link_type=WrongLinkType.WRONG_CREATE, link_label='create') - - result = self.cli_runner.invoke(cmd_database.detect_invalid_links, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - def test_detect_invalid_nodes_unknown_node_type(self): - """Test `verdi database integrity detect-invalid-nodes` when node type is invalid.""" - result = self.cli_runner.invoke(cmd_database.detect_invalid_nodes, []) - self.assertEqual(result.exit_code, 0) - self.assertClickResultNoException(result) - - # Create a node with invalid type: since there are a lot of validation rules that prevent us from creating an - # invalid node type normally, we have to do it manually on the database model instance before storing - node = Data() - node.backend_entity.dbmodel.node_type = '__main__.SubClass.' - node.store() - - result = self.cli_runner.invoke(cmd_database.detect_invalid_nodes, []) - self.assertNotEqual(result.exit_code, 0) - self.assertIsNotNone(result.exception) - - -@pytest.mark.usefixtures('aiida_profile') -def tests_database_version(run_cli_command, manager): - """Test the ``verdi database version`` command.""" - backend_manager = manager.get_backend_manager() - result = run_cli_command(cmd_database.database_version) - assert result.output_lines[0].endswith(backend_manager.get_schema_generation_database()) - assert result.output_lines[1].endswith(backend_manager.get_schema_version_backend()) - - -@pytest.mark.usefixtures('clear_database_before_test') -def tests_database_summary(aiida_localhost, run_cli_command): - """Test the ``verdi database summary`` command with the ``-verbosity`` option.""" - from aiida import orm - node = orm.Dict().store() - result = run_cli_command(cmd_database.database_summary, ['--verbosity', 'info']) - assert aiida_localhost.label in result.output - assert node.node_type in result.output diff --git a/tests/cmdline/commands/test_status.py b/tests/cmdline/commands/test_status.py index 780cfc4db4..ac47699466 100644 --- a/tests/cmdline/commands/test_status.py +++ b/tests/cmdline/commands/test_status.py @@ -62,7 +62,9 @@ def get_backend(): monkeypatch.setattr(get_manager(), 'get_backend', get_backend) result = run_cli_command(cmd_status.verdi_status, raises=True) - assert 'Database schema version is incompatible with the code: run `verdi database migrate`.' in result.output + assert 'Database schema' in result.output + assert 'is incompatible with the code.' in result.output + assert '`verdi database migrate`' in result.output assert result.exit_code is ExitCode.CRITICAL @@ -84,3 +86,14 @@ def get_backend(): assert profile.database_hostname in result.output assert str(profile.database_port) in result.output assert result.exit_code is ExitCode.CRITICAL + + +@pytest.mark.usefixtures('aiida_profile') +def tests_database_version(run_cli_command, manager): + """Test the ``verdi database version`` command.""" + backend_manager = manager.get_backend_manager() + db_gen = backend_manager.get_schema_generation_database() + db_ver = backend_manager.get_schema_version_backend() + + result = run_cli_command(cmd_status.verdi_status) + assert f'{db_gen} / {db_ver}' in result.output diff --git a/tests/cmdline/commands/test_storage.py b/tests/cmdline/commands/test_storage.py new file mode 100644 index 0000000000..da1371c18a --- /dev/null +++ b/tests/cmdline/commands/test_storage.py @@ -0,0 +1,105 @@ +# -*- 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 `verdi storage`.""" +import pytest + +from aiida.cmdline.commands import cmd_storage +from aiida.common import exceptions + + +@pytest.mark.usefixtures('clear_database_before_test') +def tests_storage_info(aiida_localhost, run_cli_command): + """Test the ``verdi storage info`` command with the ``-statistics`` option.""" + from aiida import orm + node = orm.Dict().store() + + result = run_cli_command(cmd_storage.storage_info, options=['--statistics']) + + assert aiida_localhost.label in result.output + assert node.node_type in result.output + + +def tests_storage_migrate_force(run_cli_command): + """Test the ``verdi storage migrate`` command (with force option).""" + result = run_cli_command(cmd_storage.storage_migrate, options=['--force']) + assert result.output == '' + + +def tests_storage_migrate_interactive(run_cli_command): + """Test the ``verdi storage migrate`` command (with interactive prompt).""" + from aiida.manage.manager import get_manager + profile = get_manager().get_profile() + + result = run_cli_command(cmd_storage.storage_migrate, user_input='MIGRATE NOW') + + assert 'warning' in result.output.lower() + assert profile.name in result.output + assert 'migrate now' in result.output.lower() + assert 'migration completed' in result.output.lower() + + +def tests_storage_migrate_running_daemon(run_cli_command, monkeypatch): + """Test that ``verdi storage migrate`` raises if the daemon is running.""" + from aiida.engine.daemon.client import DaemonClient + + monkeypatch.setattr(DaemonClient, 'is_daemon_running', lambda: True) + result = run_cli_command(cmd_storage.storage_migrate, raises=True) + + assert 'daemon' in result.output.lower() + assert 'running' in result.output.lower() + + +def tests_storage_migrate_cancel_prompt(run_cli_command, monkeypatch): + """Test that ``verdi storage migrate`` detects the cancelling of the interactive prompt.""" + import click + + monkeypatch.setattr(click, 'prompt', lambda text, **kwargs: exec('import click\nraise click.Abort()')) # pylint: disable=exec-used + result = run_cli_command(cmd_storage.storage_migrate, raises=True) + + assert 'aborted' in result.output.lower() + + +@pytest.mark.parametrize('raise_type', [ + exceptions.ConfigurationError, + exceptions.DatabaseMigrationError, +]) +@pytest.mark.parametrize( + 'call_kwargs', [ + { + 'raises': True, + 'user_input': 'MIGRATE NOW' + }, + { + 'raises': True, + 'options': ['--force'] + }, + ] +) +def tests_storage_migrate_raises(run_cli_command, raise_type, call_kwargs, monkeypatch): + """Test that ``verdi storage migrate`` detects errors while migrating. + + Note: it is not enough to monkeypatch the backend object returned by the method + `manager.get_backend` because the CLI function `storage_migrate` will first call + `manager._load_backend`, whichforces the re-instantiation of this object. + Instead, the class of the object needs to be patched so that all further created + objects will have the modified method. + """ + from aiida.manage.manager import get_manager + manager = get_manager() + + def mocked_migrate(self): # pylint: disable=no-self-use + raise raise_type('passed error message') + + monkeypatch.setattr(manager.get_backend().__class__, 'migrate', mocked_migrate) + result = run_cli_command(cmd_storage.storage_migrate, **call_kwargs) + + assert result.exc_info[0] is SystemExit + assert 'Critical:' in result.output + assert 'passed error message' in result.output