From 2a9fa6a515f17f5cc20bbcd6ee4dcd631117fcaf Mon Sep 17 00:00:00 2001 From: Alexander Kozlovsky Date: Thu, 23 Nov 2023 01:43:03 +0100 Subject: [PATCH] Replace sqlite_utils with db_corruption_handling.sqlite_replacement --- .../bandwidth_accounting/db/database.py | 2 +- src/tribler/core/components/component.py | 2 +- .../gigachannel_manager.py | 2 +- .../components/metadata_store/db/store.py | 2 +- .../components/reporter/exception_handler.py | 2 +- src/tribler/core/upgrade/db8_to_db10.py | 6 +- .../core/upgrade/tests/test_upgrader.py | 2 +- src/tribler/core/upgrade/upgrade.py | 2 +- .../db_corruption_handling/__init__.py | 0 .../utilities/db_corruption_handling/base.py | 59 ++++++++ .../sqlite_replacement.py} | 64 +-------- .../db_corruption_handling/tests/__init__.py | 0 .../db_corruption_handling/tests/conftest.py | 15 +++ .../db_corruption_handling/tests/test_base.py | 57 ++++++++ .../tests/test_sqlite_replacement.py | 78 +++++++++++ src/tribler/core/utilities/pony_utils.py | 14 +- .../core/utilities/tests/test_sqlite_utils.py | 127 ------------------ src/tribler/gui/upgrade_manager.py | 2 +- 18 files changed, 232 insertions(+), 204 deletions(-) create mode 100644 src/tribler/core/utilities/db_corruption_handling/__init__.py create mode 100644 src/tribler/core/utilities/db_corruption_handling/base.py rename src/tribler/core/utilities/{sqlite_utils.py => db_corruption_handling/sqlite_replacement.py} (67%) create mode 100644 src/tribler/core/utilities/db_corruption_handling/tests/__init__.py create mode 100644 src/tribler/core/utilities/db_corruption_handling/tests/conftest.py create mode 100644 src/tribler/core/utilities/db_corruption_handling/tests/test_base.py create mode 100644 src/tribler/core/utilities/db_corruption_handling/tests/test_sqlite_replacement.py delete mode 100644 src/tribler/core/utilities/tests/test_sqlite_utils.py diff --git a/src/tribler/core/components/bandwidth_accounting/db/database.py b/src/tribler/core/components/bandwidth_accounting/db/database.py index 7c414fee9ad..6594b95c9b7 100644 --- a/src/tribler/core/components/bandwidth_accounting/db/database.py +++ b/src/tribler/core/components/bandwidth_accounting/db/database.py @@ -5,8 +5,8 @@ from tribler.core.components.bandwidth_accounting.db import history, misc, transaction as db_transaction from tribler.core.components.bandwidth_accounting.db.transaction import BandwidthTransactionData +from tribler.core.utilities.db_corruption_handling.base import handle_db_if_corrupted from tribler.core.utilities.pony_utils import TriblerDatabase -from tribler.core.utilities.sqlite_utils import handle_db_if_corrupted from tribler.core.utilities.utilities import MEMORY_DB diff --git a/src/tribler/core/components/component.py b/src/tribler/core/components/component.py index 87be9c02a0d..c17ebd0c519 100644 --- a/src/tribler/core/components/component.py +++ b/src/tribler/core/components/component.py @@ -9,8 +9,8 @@ from tribler.core.components.exceptions import ComponentStartupException, MissedDependency, NoneComponent from tribler.core.components.reporter.exception_handler import default_core_exception_handler from tribler.core.sentry_reporter.sentry_reporter import SentryReporter +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted from tribler.core.utilities.exit_codes import EXITCODE_DATABASE_IS_CORRUPTED -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted from tribler.core.utilities.process_manager import get_global_process_manager if TYPE_CHECKING: diff --git a/src/tribler/core/components/gigachannel_manager/gigachannel_manager.py b/src/tribler/core/components/gigachannel_manager/gigachannel_manager.py index 07323ee6cab..529b6048e62 100644 --- a/src/tribler/core/components/gigachannel_manager/gigachannel_manager.py +++ b/src/tribler/core/components/gigachannel_manager/gigachannel_manager.py @@ -12,9 +12,9 @@ from tribler.core.components.metadata_store.db.orm_bindings.channel_node import COMMITTED from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT from tribler.core.components.metadata_store.db.store import MetadataStore +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted from tribler.core.utilities.notifier import Notifier from tribler.core.utilities.pony_utils import run_threaded -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted from tribler.core.utilities.simpledefs import DownloadStatus from tribler.core.utilities.unicode import hexlify diff --git a/src/tribler/core/components/metadata_store/db/store.py b/src/tribler/core/components/metadata_store/db/store.py index c8f63a86c8e..b8addf25816 100644 --- a/src/tribler/core/components/metadata_store/db/store.py +++ b/src/tribler/core/components/metadata_store/db/store.py @@ -46,11 +46,11 @@ from tribler.core.components.metadata_store.remote_query_community.payload_checker import process_payload from tribler.core.components.torrent_checker.torrent_checker.dataclasses import HealthInfo from tribler.core.exceptions import InvalidSignatureException +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted, handle_db_if_corrupted from tribler.core.utilities.notifier import Notifier from tribler.core.utilities.path_util import Path from tribler.core.utilities.pony_utils import TriblerDatabase, get_max, get_or_create, run_threaded from tribler.core.utilities.search_utils import torrent_rank -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted, handle_db_if_corrupted from tribler.core.utilities.unicode import hexlify from tribler.core.utilities.utilities import MEMORY_DB diff --git a/src/tribler/core/components/reporter/exception_handler.py b/src/tribler/core/components/reporter/exception_handler.py index bc80d904e61..018dcc67f3d 100644 --- a/src/tribler/core/components/reporter/exception_handler.py +++ b/src/tribler/core/components/reporter/exception_handler.py @@ -10,8 +10,8 @@ from tribler.core.components.exceptions import ComponentStartupException from tribler.core.components.reporter.reported_error import ReportedError from tribler.core.sentry_reporter.sentry_reporter import SentryReporter +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted from tribler.core.utilities.exit_codes import EXITCODE_DATABASE_IS_CORRUPTED -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted from tribler.core.utilities.process_manager import get_global_process_manager # There are some errors that we are ignoring. diff --git a/src/tribler/core/upgrade/db8_to_db10.py b/src/tribler/core/upgrade/db8_to_db10.py index 5c7abe4fe2f..68534b56456 100644 --- a/src/tribler/core/upgrade/db8_to_db10.py +++ b/src/tribler/core/upgrade/db8_to_db10.py @@ -7,7 +7,7 @@ from pony.orm import db_session from tribler.core.components.metadata_store.db.store import MetadataStore -from tribler.core.utilities import sqlite_utils +from tribler.core.utilities.db_corruption_handling import sqlite_replacement TABLE_NAMES = ( "ChannelNode", "TorrentState", "TorrentState_TrackerState", "ChannelPeer", "ChannelVote", "TrackerState", "Vsids") @@ -130,7 +130,7 @@ def do_migration(self): for table_name in TABLE_NAMES: old_table_columns[table_name] = get_table_columns(self.old_db_path, table_name) - with contextlib.closing(sqlite_utils.connect(self.new_db_path)) as connection: + with contextlib.closing(sqlite_replacement.connect(self.new_db_path)) as connection: with connection: cursor = connection.cursor() cursor.execute("PRAGMA journal_mode = OFF;") @@ -234,7 +234,7 @@ def calc_progress(duration_now, duration_half=60.0): def get_table_columns(db_path, table_name): - with contextlib.closing(sqlite_utils.connect(db_path)) as connection, connection: + with contextlib.closing(sqlite_replacement.connect(db_path)) as connection, connection: cursor = connection.cursor() cursor.execute(f'SELECT * FROM {table_name} LIMIT 1') names = [description[0] for description in cursor.description] diff --git a/src/tribler/core/upgrade/tests/test_upgrader.py b/src/tribler/core/upgrade/tests/test_upgrader.py index b73c3e5faa5..91d7bb6ab66 100644 --- a/src/tribler/core/upgrade/tests/test_upgrader.py +++ b/src/tribler/core/upgrade/tests/test_upgrader.py @@ -18,7 +18,7 @@ from tribler.core.upgrade.upgrade import TriblerUpgrader, catch_db_is_corrupted_exception, \ cleanup_noncompliant_channel_torrents from tribler.core.utilities.configparser import CallbackConfigParser -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted from tribler.core.utilities.utilities import random_infohash diff --git a/src/tribler/core/upgrade/upgrade.py b/src/tribler/core/upgrade/upgrade.py index b93cce06f4d..0a8d4e9f478 100644 --- a/src/tribler/core/upgrade/upgrade.py +++ b/src/tribler/core/upgrade/upgrade.py @@ -23,9 +23,9 @@ from tribler.core.upgrade.tags_to_knowledge.migration import MigrationTagsToKnowledge from tribler.core.upgrade.tags_to_knowledge.tags_db import TagDatabase from tribler.core.utilities.configparser import CallbackConfigParser +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted from tribler.core.utilities.path_util import Path from tribler.core.utilities.pony_utils import get_db_version -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted from tribler.core.utilities.simpledefs import STATEDIR_CHANNELS_DIR, STATEDIR_DB_DIR diff --git a/src/tribler/core/utilities/db_corruption_handling/__init__.py b/src/tribler/core/utilities/db_corruption_handling/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/tribler/core/utilities/db_corruption_handling/base.py b/src/tribler/core/utilities/db_corruption_handling/base.py new file mode 100644 index 00000000000..d4016bba6ea --- /dev/null +++ b/src/tribler/core/utilities/db_corruption_handling/base.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import logging +import sqlite3 +from contextlib import contextmanager +from pathlib import Path +from typing import Union + +logger = logging.getLogger('db_corruption_handling') + + +class DatabaseIsCorrupted(Exception): + pass + + +@contextmanager +def handling_malformed_db_error(db_filepath: Path): + # Used in all methods of Connection and Cursor classes where the database corruption error can occur + try: + yield + except Exception as e: + if _is_malformed_db_exception(e): + _mark_db_as_corrupted(db_filepath) + raise DatabaseIsCorrupted(str(db_filepath)) from e + raise + + +def handle_db_if_corrupted(db_filename: Union[str, Path]): + # Checks if the database is marked as corrupted and handles it by removing the database file and the marker file + db_path = Path(db_filename) + marker_path = get_corrupted_db_marker_path(db_path) + if marker_path.exists(): + _handle_corrupted_db(db_path) + + +def get_corrupted_db_marker_path(db_filepath: Path) -> Path: + return Path(str(db_filepath) + '.is_corrupted') + + +def _is_malformed_db_exception(exception): + return isinstance(exception, sqlite3.DatabaseError) and 'malformed' in str(exception) + + +def _mark_db_as_corrupted(db_filepath: Path): + # Creates a new `*.is_corrupted` marker file alongside the database file + marker_path = get_corrupted_db_marker_path(db_filepath) + marker_path.touch() + + +def _handle_corrupted_db(db_path: Path): + # Removes the database file and the marker file + if db_path.exists(): + logger.warning(f'Database file was marked as corrupted, removing it: {db_path}') + db_path.unlink() + + marker_path = get_corrupted_db_marker_path(db_path) + if marker_path.exists(): + logger.warning(f'Removing the corrupted database marker: {marker_path}') + marker_path.unlink() diff --git a/src/tribler/core/utilities/sqlite_utils.py b/src/tribler/core/utilities/db_corruption_handling/sqlite_replacement.py similarity index 67% rename from src/tribler/core/utilities/sqlite_utils.py rename to src/tribler/core/utilities/db_corruption_handling/sqlite_replacement.py index d03e524cdcc..9fea2c29a55 100644 --- a/src/tribler/core/utilities/sqlite_utils.py +++ b/src/tribler/core/utilities/db_corruption_handling/sqlite_replacement.py @@ -1,13 +1,12 @@ from __future__ import annotations -import logging import sqlite3 import sys -from contextlib import contextmanager from pathlib import Path from sqlite3 import DataError, DatabaseError, Error, IntegrityError, InterfaceError, InternalError, NotSupportedError, \ OperationalError, ProgrammingError, Warning, sqlite_version_info # pylint: disable=unused-import, redefined-builtin -from typing import Any, Generator, List, Literal, TypeVar, Union + +from tribler.core.utilities.db_corruption_handling.base import handling_malformed_db_error # This module serves as a replacement to the sqlite3 module and handles the case when the database is corrupted. @@ -20,63 +19,10 @@ # After that, the database is recreated upon the next attempt to connect to it. -logger = logging.getLogger(__name__) - - -class DatabaseIsCorrupted(Exception): - pass - - -@contextmanager -def _handling_malformed_db_error(db_filepath: Path): - # Used in all methods of Connection and Cursor classes where the database corruption error can occur - try: - yield - except Exception as e: - if _is_malformed_db_exception(e): - _mark_db_as_corrupted(db_filepath) - raise DatabaseIsCorrupted(str(db_filepath)) from e - raise - - -def _is_malformed_db_exception(exception): - return isinstance(exception, sqlite3.DatabaseError) and 'malformed' in str(exception) - - -def _mark_db_as_corrupted(db_filepath: Path): - # Creates a new `*.is_corrupted` marker file alongside the database file - marker_path = get_corrupted_db_marker_path(db_filepath) - marker_path.touch() - - -def get_corrupted_db_marker_path(db_filepath: Path) -> Path: - return Path(str(db_filepath) + '.is_corrupted') - - -def handle_db_if_corrupted(db_filename: Union[str, Path]): - # Checks if the database is marked as corrupted and handles it by removing the database file and the marker file - db_path = Path(db_filename) - marker_path = get_corrupted_db_marker_path(db_path) - if marker_path.exists(): - _handle_corrupted_db(db_path) - - -def _handle_corrupted_db(db_path: Path): - # Removes the database file and the marker file - if db_path.exists(): - logger.warning(f'Database file was marked as corrupted, removing it: {db_path}') - db_path.unlink() - - marker_path = get_corrupted_db_marker_path(db_path) - if marker_path.exists(): - logger.warning(f'Removing the corrupted database marker: {marker_path}') - marker_path.unlink() - - def connect(db_filename: str, **kwargs) -> sqlite3.Connection: # Replaces the sqlite3.connect function kwargs['factory'] = Connection - with _handling_malformed_db_error(Path(db_filename)): + with handling_malformed_db_error(Path(db_filename)): return sqlite3.connect(db_filename, **kwargs) @@ -84,7 +30,7 @@ def _add_method_wrapper_that_handles_malformed_db_exception(cls, method_name: st # Creates a wrapper for the given method that handles the case when the database is corrupted def wrapper(self, *args, **kwargs): - with _handling_malformed_db_error(self._db_filepath): + with handling_malformed_db_error(self._db_filepath): return getattr(super(cls, self), method_name)(*args, **kwargs) wrapper.__name__ = method_name @@ -128,7 +74,7 @@ def iterdump(self): raise NotImplementedError def blobopen(self, *args, **kwargs) -> Blob: # Works for Python >= 3.11 - with _handling_malformed_db_error(self._db_filepath): + with handling_malformed_db_error(self._db_filepath): blob = super().blobopen(*args, **kwargs) return Blob(blob, self._db_filepath) diff --git a/src/tribler/core/utilities/db_corruption_handling/tests/__init__.py b/src/tribler/core/utilities/db_corruption_handling/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/tribler/core/utilities/db_corruption_handling/tests/conftest.py b/src/tribler/core/utilities/db_corruption_handling/tests/conftest.py new file mode 100644 index 00000000000..ea2f0bfa8b1 --- /dev/null +++ b/src/tribler/core/utilities/db_corruption_handling/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from tribler.core.utilities.db_corruption_handling.sqlite_replacement import connect + + +@pytest.fixture(name='db_filepath') +def db_filepath_fixture(tmp_path): + return tmp_path / 'test.db' + + +@pytest.fixture(name='connection') +def connection_fixture(db_filepath): + connection = connect(str(db_filepath)) + yield connection + connection.close() diff --git a/src/tribler/core/utilities/db_corruption_handling/tests/test_base.py b/src/tribler/core/utilities/db_corruption_handling/tests/test_base.py new file mode 100644 index 00000000000..79448f2da0d --- /dev/null +++ b/src/tribler/core/utilities/db_corruption_handling/tests/test_base.py @@ -0,0 +1,57 @@ +import sqlite3 +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + + +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted, handle_db_if_corrupted, \ + handling_malformed_db_error + +malformed_error = sqlite3.DatabaseError('database disk image is malformed') + + +def test_handling_malformed_db_error__no_error(db_filepath): + # If no error is raised, the database should not be marked as corrupted + with handling_malformed_db_error(db_filepath): + pass + + assert not Path(str(db_filepath) + '.is_corrupted').exists() + + +def test_handling_malformed_db_error__malformed_error(db_filepath): + # Malformed database errors should be handled by marking the database as corrupted + with pytest.raises(DatabaseIsCorrupted): + with handling_malformed_db_error(db_filepath): + raise malformed_error + + assert Path(str(db_filepath) + '.is_corrupted').exists() + + +def test_handling_malformed_db_error__other_error(db_filepath): + # Other errors should not be handled like malformed database errors + class TestError(Exception): + pass + + with pytest.raises(TestError): + with handling_malformed_db_error(db_filepath): + raise TestError() + + assert not Path(str(db_filepath) + '.is_corrupted').exists() + + +def test_handle_db_if_corrupted__corrupted(db_filepath: Path): + # If the corruption marker is found, the corrupted database file is removed + marker_path = Path(str(db_filepath) + '.is_corrupted') + marker_path.touch() + + handle_db_if_corrupted(db_filepath) + assert not db_filepath.exists() + assert not marker_path.exists() + + +@patch('tribler.core.utilities.db_corruption_handling.base._handle_corrupted_db') +def test_handle_db_if_corrupted__not_corrupted(handle_corrupted_db: Mock, db_filepath: Path): + # If the corruption marker is not found, the handling of the database is not performed + handle_db_if_corrupted(db_filepath) + handle_corrupted_db.assert_not_called() diff --git a/src/tribler/core/utilities/db_corruption_handling/tests/test_sqlite_replacement.py b/src/tribler/core/utilities/db_corruption_handling/tests/test_sqlite_replacement.py new file mode 100644 index 00000000000..3a05611048f --- /dev/null +++ b/src/tribler/core/utilities/db_corruption_handling/tests/test_sqlite_replacement.py @@ -0,0 +1,78 @@ +import sqlite3 +from unittest.mock import Mock, patch + +import pytest + +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted +from tribler.core.utilities.db_corruption_handling.sqlite_replacement import Blob, Connection, \ + Cursor, _add_method_wrapper_that_handles_malformed_db_exception, connect + + +# pylint: disable=protected-access + + +malformed_error = sqlite3.DatabaseError('database disk image is malformed') + + +def test_connect(db_filepath): + connection = connect(str(db_filepath)) + assert isinstance(connection, Connection) + connection.close() + + +def test_make_method_that_handles_malformed_db_exception(db_filepath): + # Tests that the _make_method_that_handles_malformed_db_exception function creates a method that handles + # the malformed database exception + + class BaseClass: + method1 = Mock(return_value=Mock()) + + class TestClass(BaseClass): + pass + + _add_method_wrapper_that_handles_malformed_db_exception(TestClass, 'method1') + + # The method should be successfully wrapped + assert TestClass.method1.is_wrapped + assert TestClass.method1.__name__ == 'method1' + + test_instance = TestClass() + test_instance._db_filepath = db_filepath + result = test_instance.method1(1, 2, x=3, y=4) + + # *args and **kwargs should be passed to the original method, and the result should be returned + BaseClass.method1.assert_called_once_with(1, 2, x=3, y=4) + assert result is BaseClass.method1.return_value + + # When the base method raises a malformed database exception, the DatabaseIsCorrupted exception should be raised + BaseClass.method1.side_effect = malformed_error + with pytest.raises(DatabaseIsCorrupted): + test_instance.method1(1, 2, x=3, y=4) + + +def test_connection_cursor(connection): + cursor = connection.cursor() + assert isinstance(cursor, Cursor) + + +def test_connection_iterdump(connection): + with pytest.raises(NotImplementedError): + connection.iterdump() + + +@patch('tribler.core.utilities.db_corruption_handling.sqlite_replacement.ConnectionBase.blobopen', + Mock(side_effect=malformed_error)) +def test_connection_blobopen__exception(connection): + with pytest.raises(DatabaseIsCorrupted): + connection.blobopen() + + +@patch('tribler.core.utilities.db_corruption_handling.sqlite_replacement.ConnectionBase.blobopen') +def test_connection_blobopen__no_exception(blobopen, connection): + blobopen.return_value = Mock() + result = connection.blobopen() + + blobopen.assert_called_once() + assert isinstance(result, Blob) + assert result._blob is blobopen.return_value + assert result._db_filepath == connection._db_filepath diff --git a/src/tribler/core/utilities/pony_utils.py b/src/tribler/core/utilities/pony_utils.py index fe2350c056a..3d4c7d30fd8 100644 --- a/src/tribler/core/utilities/pony_utils.py +++ b/src/tribler/core/utilities/pony_utils.py @@ -20,8 +20,8 @@ from pony.orm.core import Database, select from pony.orm.dbproviders import sqlite from pony.utils import cut_traceback, localbase -from tribler.core.utilities import sqlite_utils - +from tribler.core.utilities.db_corruption_handling import sqlite_replacement +from tribler.core.utilities.db_corruption_handling.base import handle_db_if_corrupted # Inject sqlite replacement to PonyORM sqlite database provider to use augmented version of Connection and Cursor # classes that handle database corruption errors. All connection and cursor methods, such as execute and fetchone, @@ -29,7 +29,7 @@ # extension is created alongside the corrupted database file. As a result of exception, the Tribler Core immediately # stops with the error code 99. Tribler GUI handles this error code by showing the message to the user and automatically # restarting the Core. After the Core is restarted, the database is re-created from scratch. -sqlite.sqlite = sqlite_utils +sqlite.sqlite = sqlite_replacement SLOW_DB_SESSION_DURATION_THRESHOLD = 1.0 @@ -41,17 +41,17 @@ StatDict = Dict[Optional[str], core.QueryStat] -def table_exists(cursor: sqlite_utils.Cursor, table_name: str) -> bool: +def table_exists(cursor: sqlite_replacement.Cursor, table_name: str) -> bool: cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)) return cursor.fetchone() is not None def get_db_version(db_path, default: int = None) -> int: - sqlite_utils.handle_db_if_corrupted(db_path) + handle_db_if_corrupted(db_path) version = None if db_path.exists(): - with contextlib.closing(sqlite_utils.connect(db_path)) as connection: + with contextlib.closing(sqlite_replacement.connect(db_path)) as connection: with connection: cursor = connection.cursor() if table_exists(cursor, 'MiscData'): @@ -364,7 +364,7 @@ def bind(self, **kwargs): if not db_path.absolute(): raise ValueError(f"The 'filename' attribute is expected to be an absolute path. Got: {filename}") - sqlite_utils.handle_db_if_corrupted(db_path) + handle_db_if_corrupted(db_path) self._bind(TriblerSQLiteProvider, **kwargs) diff --git a/src/tribler/core/utilities/tests/test_sqlite_utils.py b/src/tribler/core/utilities/tests/test_sqlite_utils.py deleted file mode 100644 index 7b0c1017935..00000000000 --- a/src/tribler/core/utilities/tests/test_sqlite_utils.py +++ /dev/null @@ -1,127 +0,0 @@ -from pathlib import Path -from sqlite3 import DatabaseError -from unittest.mock import Mock, patch - -import pytest - -from tribler.core.utilities.sqlite_utils import Blob, Connection, Cursor, DatabaseIsCorrupted, \ - _add_method_wrapper_that_handles_malformed_db_exception, connect, \ - handle_db_if_corrupted, _handling_malformed_db_error - - -# pylint: disable=protected-access - - -@pytest.fixture(name='db_filepath') -def db_filepath_fixture(tmp_path): - return tmp_path / 'test.db' - - -@pytest.fixture(name='connection') -def connection_fixture(db_filepath): - connection = connect(str(db_filepath)) - yield connection - connection.close() - - -malformed_error = DatabaseError('database disk image is malformed') - - -def test_handling_malformed_db_error__malformed_error(db_filepath): - with pytest.raises(DatabaseIsCorrupted): - with _handling_malformed_db_error(db_filepath): - raise malformed_error - assert Path(str(db_filepath) + '.is_corrupted').exists() - - -def test_handling_malformed_db_error__other_error(db_filepath): - with pytest.raises(ZeroDivisionError): - with _handling_malformed_db_error(db_filepath): - raise ZeroDivisionError() - assert not Path(str(db_filepath) + '.is_corrupted').exists() - - -def test_handling_malformed_db_error__no_error(db_filepath): - with _handling_malformed_db_error(db_filepath): - pass - assert not Path(str(db_filepath) + '.is_corrupted').exists() - - -@patch('tribler.core.utilities.sqlite_utils._handle_corrupted_db') -def test_handle_db_if_corrupted__not_corrupted(handle_corrupted_db: Mock, db_filepath: Path): - # If the corruption marker is not found, the handling of the database is not performed - handle_db_if_corrupted(db_filepath) - handle_corrupted_db.assert_not_called() - - -def test_handle_db_if_corrupted__corrupted(db_filepath: Path): - # If the corruption marker is found, the corrupted database file is removed - marker_path = Path(str(db_filepath) + '.is_corrupted') - marker_path.touch() - - handle_db_if_corrupted(db_filepath) - assert not db_filepath.exists() - assert not marker_path.exists() - - -def test_connect(db_filepath): - connection = connect(str(db_filepath)) - assert isinstance(connection, Connection) - connection.close() - - -def test_make_method_that_handles_malformed_db_exception(db_filepath): - # Tests that the _make_method_that_handles_malformed_db_exception function creates a method that handles - # the malformed database exception - - class BaseClass: - method1 = Mock(return_value=Mock()) - - class TestClass(BaseClass): - pass - - _add_method_wrapper_that_handles_malformed_db_exception(TestClass, 'method1') - - # The method should be successfully wrapped - assert TestClass.method1.is_wrapped - assert TestClass.method1.__name__ == 'method1' - - test_instance = TestClass() - test_instance._db_filepath = db_filepath - result = test_instance.method1(1, 2, x=3, y=4) - - # *args and **kwargs should be passed to the original method, and the result should be returned - BaseClass.method1.assert_called_once_with(1, 2, x=3, y=4) - assert result is BaseClass.method1.return_value - - # When the base method raises a malformed database exception, the DatabaseIsCorrupted exception should be raised - BaseClass.method1.side_effect = malformed_error - with pytest.raises(DatabaseIsCorrupted): - test_instance.method1(1, 2, x=3, y=4) - - -def test_connection_cursor(connection): - cursor = connection.cursor() - assert isinstance(cursor, Cursor) - - -def test_connection_iterdump(connection): - with pytest.raises(NotImplementedError): - connection.iterdump() - - -@patch('tribler.core.utilities.sqlite_utils.ConnectionBase.blobopen', Mock(side_effect=malformed_error)) -def test_connection_blobopen__exception(connection): - with pytest.raises(DatabaseIsCorrupted): - connection.blobopen() - - -@patch('tribler.core.utilities.sqlite_utils.ConnectionBase.blobopen') -def test_connection_blobopen__no_exception(blobopen, connection): - blobopen.return_value = Mock() - result = connection.blobopen() - - blobopen.assert_called_once() - assert isinstance(result, Blob) - assert result._blob is blobopen.return_value - assert result._db_filepath == connection._db_filepath diff --git a/src/tribler/gui/upgrade_manager.py b/src/tribler/gui/upgrade_manager.py index 961b7dbe353..fea476870ac 100644 --- a/src/tribler/gui/upgrade_manager.py +++ b/src/tribler/gui/upgrade_manager.py @@ -11,7 +11,7 @@ from tribler.core.config.tribler_config import TriblerConfig from tribler.core.upgrade.upgrade import TriblerUpgrader from tribler.core.upgrade.version_manager import TriblerVersion, VersionHistory, NoDiskSpaceAvailableError -from tribler.core.utilities.sqlite_utils import DatabaseIsCorrupted +from tribler.core.utilities.db_corruption_handling.base import DatabaseIsCorrupted from tribler.gui.defs import BUTTON_TYPE_NORMAL, CORRUPTED_DB_WAS_FIXED_MESSAGE, NO_DISK_SPACE_ERROR_MESSAGE, \ UPGRADE_CANCELLED_ERROR_TITLE from tribler.gui.dialogs.confirmationdialog import ConfirmationDialog