Start writing tests for inventory #2165

Merged
PeterSurda merged 8 commits from gitea-39 into v0.6 2023-11-21 16:51:05 +01:00
5 changed files with 132 additions and 95 deletions

View File

@ -7,6 +7,16 @@ from bmconfigparser import config
from singleton import Singleton from singleton import Singleton
def create_inventory_instance(backend="sqlite"):
"""
Create an instance of the inventory class
defined in `storage.<backend>`.
"""
return getattr(
getattr(storage, backend),
"{}Inventory".format(backend.title()))()
@Singleton @Singleton
class Inventory(): class Inventory():
""" """
@ -15,11 +25,7 @@ class Inventory():
""" """
def __init__(self): def __init__(self):
self._moduleName = config.safeGet("inventory", "storage") self._moduleName = config.safeGet("inventory", "storage")
self._inventoryClass = getattr( self._realInventory = create_inventory_instance(self._moduleName)
getattr(storage, self._moduleName),
"{}Inventory".format(self._moduleName.title())
)
self._realInventory = self._inventoryClass()
self.numberOfInventoryLookupsPerformed = 0 self.numberOfInventoryLookupsPerformed = 0
# cheap inheritance copied from asyncore # cheap inheritance copied from asyncore

View File

@ -2,21 +2,19 @@
Module for using filesystem (directory with files) for inventory storage Module for using filesystem (directory with files) for inventory storage
""" """
import logging import logging
import string import os
import time import time
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from os import listdir, makedirs, path, remove, rmdir
from threading import RLock from threading import RLock
from paths import lookupAppdataFolder from paths import lookupAppdataFolder
from storage import InventoryItem, InventoryStorage from .storage import InventoryItem, InventoryStorage
logger = logging.getLogger('default') logger = logging.getLogger('default')
class FilesystemInventory(InventoryStorage): class FilesystemInventory(InventoryStorage):
"""Filesystem for inventory storage""" """Filesystem for inventory storage"""
# pylint: disable=too-many-ancestors, abstract-method
topDir = "inventory" topDir = "inventory"
objectDir = "objects" objectDir = "objects"
metadataFilename = "metadata" metadataFilename = "metadata"
@ -24,15 +22,15 @@ class FilesystemInventory(InventoryStorage):
def __init__(self): def __init__(self):
super(FilesystemInventory, self).__init__() super(FilesystemInventory, self).__init__()
self.baseDir = path.join( self.baseDir = os.path.join(
lookupAppdataFolder(), FilesystemInventory.topDir) lookupAppdataFolder(), FilesystemInventory.topDir)
for createDir in [self.baseDir, path.join(self.baseDir, "objects")]: for createDir in [self.baseDir, os.path.join(self.baseDir, "objects")]:
if path.exists(createDir): if os.path.exists(createDir):
if not path.isdir(createDir): if not os.path.isdir(createDir):
raise IOError( raise IOError(
"%s exists but it's not a directory" % createDir) "%s exists but it's not a directory" % createDir)
else: else:
makedirs(createDir) os.makedirs(createDir)
# Guarantees that two receiveDataThreads # Guarantees that two receiveDataThreads
# don't receive and process the same message # don't receive and process the same message
# concurrently (probably sent by a malicious individual) # concurrently (probably sent by a malicious individual)
@ -46,6 +44,9 @@ class FilesystemInventory(InventoryStorage):
return True return True
return False return False
def __delitem__(self, hash_):
raise NotImplementedError
def __getitem__(self, hashval): def __getitem__(self, hashval):
for streamDict in self._inventory.values(): for streamDict in self._inventory.values():
try: try:
@ -66,18 +67,18 @@ class FilesystemInventory(InventoryStorage):
with self.lock: with self.lock:
value = InventoryItem(*value) value = InventoryItem(*value)
try: try:
makedirs(path.join( os.makedirs(os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashval))) hexlify(hashval).decode()))
except OSError: except OSError:
pass pass
try: try:
with open( with open(
path.join( os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashval), hexlify(hashval).decode(),
FilesystemInventory.metadataFilename, FilesystemInventory.metadataFilename,
), ),
"w", "w",
@ -86,15 +87,15 @@ class FilesystemInventory(InventoryStorage):
value.type, value.type,
value.stream, value.stream,
value.expires, value.expires,
hexlify(value.tag))) hexlify(value.tag).decode()))
with open( with open(
path.join( os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashval), hexlify(hashval).decode(),
FilesystemInventory.dataFilename, FilesystemInventory.dataFilename,
), ),
"w", "wb",
) as f: ) as f:
f.write(value.payload) f.write(value.payload)
except IOError: except IOError:
@ -114,28 +115,28 @@ class FilesystemInventory(InventoryStorage):
pass pass
with self.lock: with self.lock:
try: try:
remove( os.remove(
path.join( os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashval), hexlify(hashval).decode(),
FilesystemInventory.metadataFilename)) FilesystemInventory.metadataFilename))
except IOError: except IOError:
pass pass
try: try:
remove( os.remove(
path.join( os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashval), hexlify(hashval).decode(),
FilesystemInventory.dataFilename)) FilesystemInventory.dataFilename))
except IOError: except IOError:
pass pass
try: try:
rmdir(path.join( os.rmdir(os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashval))) hexlify(hashval).decode()))
except IOError: except IOError:
pass pass
@ -168,8 +169,6 @@ class FilesystemInventory(InventoryStorage):
logger.debug( logger.debug(
'error loading %s', hexlify(hashId), exc_info=True) 'error loading %s', hexlify(hashId), exc_info=True)
self._inventory = newInventory self._inventory = newInventory
# for i, v in self._inventory.items():
# print "loaded stream: %s, %i items" % (i, len(v))
def stream_list(self): def stream_list(self):
"""Return list of streams""" """Return list of streams"""
@ -177,17 +176,17 @@ class FilesystemInventory(InventoryStorage):
def object_list(self): def object_list(self):
"""Return inventory vectors (hashes) from a directory""" """Return inventory vectors (hashes) from a directory"""
return [unhexlify(x) for x in listdir(path.join( return [unhexlify(x) for x in os.listdir(os.path.join(
self.baseDir, FilesystemInventory.objectDir))] self.baseDir, FilesystemInventory.objectDir))]
def getData(self, hashId): def getData(self, hashId):
"""Get object data""" """Get object data"""
try: try:
with open( with open(
path.join( os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashId), hexlify(hashId).decode(),
FilesystemInventory.dataFilename, FilesystemInventory.dataFilename,
), ),
"r", "r",
@ -200,16 +199,16 @@ class FilesystemInventory(InventoryStorage):
"""Get object metadata""" """Get object metadata"""
try: try:
with open( with open(
path.join( os.path.join(
self.baseDir, self.baseDir,
FilesystemInventory.objectDir, FilesystemInventory.objectDir,
hexlify(hashId), hexlify(hashId).decode(),
FilesystemInventory.metadataFilename, FilesystemInventory.metadataFilename,
), ),
"r", "r",
) as f: ) as f:
objectType, streamNumber, expiresTime, tag = string.split( objectType, streamNumber, expiresTime, tag = f.read().split(
f.read(), ",", 4)[:4] ",", 4)[:4]
return [ return [
int(objectType), int(objectType),
int(streamNumber), int(streamNumber),
@ -246,10 +245,10 @@ class FilesystemInventory(InventoryStorage):
def unexpired_hashes_by_stream(self, stream): def unexpired_hashes_by_stream(self, stream):
"""Return unexpired hashes in the inventory for a particular stream""" """Return unexpired hashes in the inventory for a particular stream"""
t = int(time.time())
try: try:
return [x for x, value in self._inventory[stream].items() return [
if value.expires > t] x for x, value in self._inventory[stream].items()
if value.expires > int(time.time())]
except KeyError: except KeyError:
return [] return []
@ -259,7 +258,7 @@ class FilesystemInventory(InventoryStorage):
def clean(self): def clean(self):
"""Clean out old items from the inventory""" """Clean out old items from the inventory"""
minTime = int(time.time()) - (60 * 60 * 30) minTime = int(time.time()) - 60 * 60 * 30
deletes = [] deletes = []
for streamDict in self._inventory.values(): for streamDict in self._inventory.values():
for hashId, item in streamDict.items(): for hashId, item in streamDict.items():

View File

@ -6,10 +6,10 @@ import time
from threading import RLock from threading import RLock
from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery
from storage import InventoryItem, InventoryStorage from .storage import InventoryItem, InventoryStorage
class SqliteInventory(InventoryStorage): # pylint: disable=too-many-ancestors class SqliteInventory(InventoryStorage):
"""Inventory using SQLite""" """Inventory using SQLite"""
def __init__(self): def __init__(self):
super(SqliteInventory, self).__init__() super(SqliteInventory, self).__init__()

View File

@ -1,73 +1,47 @@
""" """
Storing inventory items Storing inventory items
""" """
import collections
InventoryItem = collections.namedtuple( from abc import abstractmethod
'InventoryItem', 'type stream payload expires tag') from collections import namedtuple
try:
from collections import MutableMapping # pylint: disable=deprecated-class
except ImportError:
from collections.abc import MutableMapping
class Storage(object): # pylint: disable=too-few-public-methods InventoryItem = namedtuple('InventoryItem', 'type stream payload expires tag')
"""Base class for storing inventory
(extendable for other items to store)"""
pass
class InventoryStorage(Storage, collections.MutableMapping): class InventoryStorage(MutableMapping):
"""Module used for inventory storage""" """
Base class for storing inventory
(extendable for other items to store)
"""
def __init__(self): # pylint: disable=super-init-not-called def __init__(self):
self.numberOfInventoryLookupsPerformed = 0 self.numberOfInventoryLookupsPerformed = 0
def __contains__(self, _): @abstractmethod
raise NotImplementedError def __contains__(self, item):
pass
def __getitem__(self, _):
raise NotImplementedError
def __setitem__(self, _, value):
raise NotImplementedError
def __delitem__(self, _):
raise NotImplementedError
def __iter__(self):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
@abstractmethod
def by_type_and_tag(self, objectType, tag): def by_type_and_tag(self, objectType, tag):
"""Return objects filtered by object type and tag""" """Return objects filtered by object type and tag"""
raise NotImplementedError pass
@abstractmethod
def unexpired_hashes_by_stream(self, stream): def unexpired_hashes_by_stream(self, stream):
"""Return unexpired inventory vectors filtered by stream""" """Return unexpired inventory vectors filtered by stream"""
raise NotImplementedError pass
@abstractmethod
def flush(self): def flush(self):
"""Flush cache""" """Flush cache"""
raise NotImplementedError pass
@abstractmethod
def clean(self): def clean(self):
"""Free memory / perform garbage collection""" """Free memory / perform garbage collection"""
raise NotImplementedError pass
class MailboxStorage(Storage, collections.MutableMapping):
"""Method for storing mails"""
def __delitem__(self, key):
raise NotImplementedError
def __getitem__(self, key):
raise NotImplementedError
def __iter__(self):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
def __setitem__(self, key, value):
raise NotImplementedError

View File

@ -0,0 +1,58 @@
"""Tests for inventory"""
import os
import shutil
import struct
import tempfile
import time
import unittest
from pybitmessage.storage import storage
from pybitmessage.addresses import calculateInventoryHash
from .partial import TestPartialRun
class TestFilesystemInventory(TestPartialRun):
"""A test case for the inventory using filesystem backend"""
@classmethod
def setUpClass(cls):
cls.home = os.environ['BITMESSAGE_HOME'] = tempfile.mkdtemp()
super(TestFilesystemInventory, cls).setUpClass()
from inventory import create_inventory_instance
cls.inventory = create_inventory_instance('filesystem')
def test_consistency(self):
"""Ensure the inventory is of proper class"""
if os.path.isfile(os.path.join(self.home, 'messages.dat')):
# this will likely never happen
self.fail("Failed to configure filesystem inventory!")
def test_appending(self):
"""Add a sample message to the inventory"""
TTL = 24 * 60 * 60
embedded_time = int(time.time() + TTL)
msg = struct.pack('>Q', embedded_time) + os.urandom(166)
invhash = calculateInventoryHash(msg)
self.inventory[invhash] = (2, 1, msg, embedded_time, b'')
@classmethod
def tearDownClass(cls):
super(TestFilesystemInventory, cls).tearDownClass()
cls.inventory.flush()
shutil.rmtree(os.path.join(cls.home, cls.inventory.topDir))
class TestStorageAbstract(unittest.TestCase):
"""A test case for refactoring of the storage abstract classes"""
def test_inventory_storage(self):
"""Check inherited abstract methods"""
with self.assertRaisesRegexp(
TypeError, "^Can't instantiate abstract class.*"
"methods __contains__, __delitem__, __getitem__, __iter__,"
" __len__, __setitem__"
): # pylint: disable=abstract-class-instantiated
storage.InventoryStorage()