diff --git a/src/mock/__init__.py b/src/tests/mock/__init__.py similarity index 100% rename from src/mock/__init__.py rename to src/tests/mock/__init__.py diff --git a/src/tests/mock/bitmessagemock.py b/src/tests/mock/bitmessagemock.py new file mode 100644 index 00000000..c58c9c88 --- /dev/null +++ b/src/tests/mock/bitmessagemock.py @@ -0,0 +1,52 @@ +""" +Bitmessage mock +""" +from pybitmessage.class_addressGenerator import addressGenerator +from pybitmessage.class_singleWorker import singleWorker +from pybitmessage.class_objectProcessor import objectProcessor +from pybitmessage.inventory import Inventory +from pybitmessage.bmconfigparser import BMConfigParser + + +# pylint: disable=too-few-public-methods,no-init,old-style-class +class MockMain: + """Mock main function""" + + # pylint: disable=no-self-use + def start(self): + """Start main application""" + # pylint: disable=too-many-statements,too-many-branches,too-many-locals, unused-variable + config = BMConfigParser() + daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') + + # Start the address generation thread + addressGeneratorThread = addressGenerator() + # close the main program even if there are threads left + addressGeneratorThread.daemon = True + addressGeneratorThread.start() + + # Start the thread that calculates POWs + singleWorkerThread = singleWorker() + # close the main program even if there are threads left + singleWorkerThread.daemon = True + singleWorkerThread.start() + + # Start the thread that calculates POWs + objectProcessorThread = objectProcessor() + # DON'T close the main program even the thread remains. + # This thread checks the shutdown variable after processing + # each object. + objectProcessorThread.daemon = False + objectProcessorThread.start() + + Inventory() # init + + +def main(): + """Triggers main module""" + mainprogram = MockMain() + mainprogram.start() + + +if __name__ == "__main__": + main() diff --git a/src/mock/kivy_main.py b/src/tests/mock/kivy_main.py similarity index 70% rename from src/mock/kivy_main.py rename to src/tests/mock/kivy_main.py index badc1dc1..9c68f3af 100644 --- a/src/mock/kivy_main.py +++ b/src/tests/mock/kivy_main.py @@ -1,14 +1,14 @@ """Mock kivy app with mock threads.""" from pybitmessage import state -from pybitmessage.bitmessagekivy.mpybit import NavigateApp -from class_addressGenerator import FakeAddressGenerator +from pybitmessage.mpybit import NavigateApp +from pybitmessage.class_addressGenerator import addressGenerator def main(): """main method for starting threads""" # Start the address generation thread - addressGeneratorThread = FakeAddressGenerator() + addressGeneratorThread = addressGenerator() # close the main program even if there are threads left addressGeneratorThread.daemon = True addressGeneratorThread.start() diff --git a/src/tests/mock/pybitmessage/__init__.py b/src/tests/mock/pybitmessage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/mock/pybitmessage/bmconfigparser.py b/src/tests/mock/pybitmessage/bmconfigparser.py new file mode 100644 index 00000000..efeac69b --- /dev/null +++ b/src/tests/mock/pybitmessage/bmconfigparser.py @@ -0,0 +1,273 @@ +""" +BMConfigParser class definition and default configuration settings +""" + +import os +import shutil +import sys # FIXME: bad style! write more generally +from datetime import datetime + +from six import string_types +from six.moves import configparser + +try: + import state + from singleton import Singleton +except ImportError: + from pybitmessage import state + from pybitmessage.singleton import Singleton + +SafeConfigParser = configparser.SafeConfigParser + + +BMConfigDefaults = { + "bitmessagesettings": { + "maxaddrperstreamsend": 500, + "maxbootstrapconnections": 20, + "maxdownloadrate": 0, + "maxoutboundconnections": 8, + "maxtotalconnections": 200, + "maxuploadrate": 0, + "apiinterface": "127.0.0.1", + "apiport": 8442, + "udp": "True" + }, + "threads": { + "receive": 3, + }, + "network": { + "bind": "", + "dandelion": 90, + }, + "inventory": { + "storage": "sqlite", + "acceptmismatch": "False", + }, + "knownnodes": { + "maxnodes": 20000, + }, + "zlib": { + "maxsize": 1048576 + } +} + + +@Singleton +class BMConfigParser(SafeConfigParser): + """ + Singleton class inherited from :class:`ConfigParser.SafeConfigParser` + with additional methods specific to bitmessage config. + """ + # pylint: disable=too-many-ancestors + _temp = {} + + def set(self, section, option, value=None): + if self._optcre is self.OPTCRE or value: + if not isinstance(value, string_types): + raise TypeError("option values must be strings") + if not self.validate(section, option, value): + raise ValueError("Invalid value %s" % value) + return SafeConfigParser.set(self, section, option, value) + + # pylint: disable=redefined-builtinm, too-many-return-statements + def get(self, section, option, raw=False, vars=None): + if sys.version_info[0] == 3: + # pylint: disable=arguments-differ + try: + if section == "bitmessagesettings" and option == "timeformat": + return SafeConfigParser.get( + self, section, option, raw=True, vars=vars) + try: + return self._temp[section][option] + except KeyError: + pass + return SafeConfigParser.get( + self, section, option, raw=True, vars=vars) + except configparser.InterpolationError: + return SafeConfigParser.get( + self, section, option, raw=True, vars=vars) + except (configparser.NoSectionError, configparser.NoOptionError) as e: + try: + return BMConfigDefaults[section][option] + except (KeyError, ValueError, AttributeError): + raise e + else: + # pylint: disable=arguments-differ + try: + if section == "bitmessagesettings" and option == "timeformat": + return SafeConfigParser.get( + self, section, option, raw, vars) + try: + return self._temp[section][option] + except KeyError: + pass + return SafeConfigParser.get( + self, section, option, True, vars) + except configparser.InterpolationError: + return SafeConfigParser.get( + self, section, option, True, vars) + except (configparser.NoSectionError, configparser.NoOptionError) as e: + try: + return BMConfigDefaults[section][option] + except (KeyError, ValueError, AttributeError): + raise e + + def setTemp(self, section, option, value=None): + """Temporary set option to value, not saving.""" + try: + self._temp[section][option] = value + except KeyError: + self._temp[section] = {option: value} + + def safeGetBoolean(self, section, field): + """Return value as boolean, False on exceptions""" + try: + # Used in the python2.7 + # return self.getboolean(section, field) + # Used in the python3.5.2 + # print(config, section, field) + return self.getboolean(section, field) + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): + return False + + def safeGetInt(self, section, field, default=0): + """Return value as integer, default on exceptions, + 0 if default missing""" + try: + # Used in the python2.7 + # return self.getint(section, field) + # Used in the python3.7.0 + return int(self.get(section, field)) + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): + return default + + def safeGetFloat(self, section, field, default=0.0): + """Return value as float, default on exceptions, + 0.0 if default missing""" + try: + return self.getfloat(section, field) + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): + return default + + def safeGet(self, section, option, default=None): + """Return value as is, default on exceptions, None if default missing""" + try: + return self.get(section, option) + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): + return default + + def items(self, section, raw=False, variables=None): + # pylint: disable=signature-differs + """Return section variables as parent, + but override the "raw" argument to always True""" + return SafeConfigParser.items(self, section, True, variables) + + def _reset(self): + """Reset current config. There doesn't appear to be a built in + method for this""" + sections = self.sections() + for x in sections: + self.remove_section(x) + + if sys.version_info[0] == 3: + @staticmethod + def addresses(hidden=False): + """Return a list of local bitmessage addresses (from section labels)""" + return [x for x in BMConfigParser().sections() if x.startswith('BM-') and ( + hidden or not BMConfigParser().safeGetBoolean(x, 'hidden'))] + + def read(self, filenames): + self._reset() + SafeConfigParser.read(self, filenames) + for section in self.sections(): + for option in self.options(section): + try: + # pylint: disable=unsubscriptable-object + if not self.validate( + section, option, + self[section][option] + ): + try: + newVal = BMConfigDefaults[section][option] + except configparser.NoSectionError: + continue + except KeyError: + continue + SafeConfigParser.set( + self, section, option, newVal) + except configparser.InterpolationError: + continue + + def readfp(self, fp, filename=None): + # pylint: disable=no-member + SafeConfigParser.read_file(self, fp) + else: + @staticmethod + def addresses(): + """Return a list of local bitmessage addresses (from section labels)""" + return [ + x for x in BMConfigParser().sections() if x.startswith('BM-')] + + def read(self, filenames): + """Read config and populate defaults""" + self._reset() + SafeConfigParser.read(self, filenames) + for section in self.sections(): + for option in self.options(section): + try: + if not self.validate( + section, option, + SafeConfigParser.get(self, section, option) + ): + try: + newVal = BMConfigDefaults[section][option] + except KeyError: + continue + SafeConfigParser.set( + self, section, option, newVal) + except configparser.InterpolationError: + continue + + def save(self): + """Save the runtime config onto the filesystem""" + fileName = os.path.join(state.appdata, 'keys.dat') + fileNameBak = '.'.join([ + fileName, datetime.now().strftime("%Y%j%H%M%S%f"), 'bak']) + # create a backup copy to prevent the accidental loss due to + # the disk write failure + try: + shutil.copyfile(fileName, fileNameBak) + # The backup succeeded. + fileNameExisted = True + except (IOError, Exception): + # The backup failed. This can happen if the file + # didn't exist before. + fileNameExisted = False + + with open(fileName, 'w') as configfile: + self.write(configfile) + # delete the backup + if fileNameExisted: + os.remove(fileNameBak) + + def validate(self, section, option, value): + """Input validator interface (using factory pattern)""" + try: + return getattr(self, 'validate_%s_%s' % (section, option))(value) + except AttributeError: + return True + + @staticmethod + def validate_bitmessagesettings_maxoutboundconnections(value): + """Reject maxoutboundconnections that are too high or too low""" + try: + value = int(value) + except ValueError: + return False + if value < 0 or value > 8: + return False + return True diff --git a/src/mock/class_addressGenerator.py b/src/tests/mock/pybitmessage/class_addressGenerator.py similarity index 77% rename from src/mock/class_addressGenerator.py rename to src/tests/mock/pybitmessage/class_addressGenerator.py index fbb34710..e5f59675 100644 --- a/src/mock/class_addressGenerator.py +++ b/src/tests/mock/pybitmessage/class_addressGenerator.py @@ -2,10 +2,6 @@ A thread for creating addresses """ -import logging -import random -import threading - from six.moves import queue from pybitmessage import state @@ -13,7 +9,7 @@ from pybitmessage import queues from pybitmessage.bmconfigparser import BMConfigParser -# from network.threads import StoppableThread +from pybitmessage.threads import StoppableThread fake_addresses = { @@ -40,37 +36,18 @@ fake_addresses = { } -class StoppableThread(threading.Thread): - """Base class for application threads with stopThread method""" - name = None - logger = logging.getLogger('default') - - def __init__(self, name=None): - if name: - self.name = name - super(StoppableThread, self).__init__(name=self.name) - self.stop = threading.Event() - self._stopped = False - random.seed() - self.logger.info('Init thread %s', self.name) - - def stopThread(self): - """Stop the thread""" - self._stopped = True - self.stop.set() - - -class FakeAddressGenerator(StoppableThread): +class addressGenerator(StoppableThread): """A thread for creating fake addresses""" name = "addressGenerator" address_list = list(fake_addresses.keys()) def stopThread(self): + """"To stop address generator thread""" try: queues.addressGeneratorQueue.put(("stopThread", "data")) except queue.Full: self.logger.warning('addressGeneratorQueue is Full') - super(FakeAddressGenerator, self).stopThread() + super(addressGenerator, self).stopThread() def run(self): """ diff --git a/src/tests/mock/pybitmessage/class_objectProcessor.py b/src/tests/mock/pybitmessage/class_objectProcessor.py new file mode 100644 index 00000000..8f6f072d --- /dev/null +++ b/src/tests/mock/pybitmessage/class_objectProcessor.py @@ -0,0 +1,52 @@ +""" +The objectProcessor thread, of which there is only one, +processes the network objects +""" +import logging +import random +import threading + +import queues +import state + +# from helper_sql import sql_ready, sqlExecute, sqlQuery +# from network import bmproto + +logger = logging.getLogger('default') + + +class objectProcessor(threading.Thread): + """ + The objectProcessor thread, of which there is only one, receives network + objects (msg, broadcast, pubkey, getpubkey) from the receiveDataThreads. + """ + def __init__(self): + threading.Thread.__init__(self, name="objectProcessor") + random.seed() + # It may be the case that the last time Bitmessage was running, + # the user closed it before it finished processing everything in the + # objectProcessorQueue. Assuming that Bitmessage wasn't closed + # forcefully, it should have saved the data in the queue into the + # objectprocessorqueue table. Let's pull it out. + + # sql_ready.wait() + # queryreturn = sqlQuery( + # 'SELECT objecttype, data FROM objectprocessorqueue') + # for objectType, data in queryreturn: + # queues.objectProcessorQueue.put((objectType, data)) + # sqlExecute('DELETE FROM objectprocessorqueue') + # logger.debug( + # 'Loaded %s objects from disk into the objectProcessorQueue.', + # len(queryreturn)) + # self._ack_obj = bmproto.BMStringParser() + self.successfullyDecryptMessageTimings = [] + + def run(self): + """Process the objects from `.queues.objectProcessorQueue`""" + while True: + # pylint: disable=unused-variable + objectType, data = queues.objectProcessorQueue.get() + + if state.shutdown: + state.shutdown = 2 + break diff --git a/src/tests/mock/pybitmessage/class_singleWorker.py b/src/tests/mock/pybitmessage/class_singleWorker.py new file mode 100644 index 00000000..617e835f --- /dev/null +++ b/src/tests/mock/pybitmessage/class_singleWorker.py @@ -0,0 +1,44 @@ +""" +Thread for performing PoW +""" + +from __future__ import division + +from six.moves import queue + +from pybitmessage import state +from pybitmessage import queues +from pybitmessage.threads import StoppableThread + + +class singleWorker(StoppableThread): + """Thread for performing PoW""" + + def __init__(self): + super(singleWorker, self).__init__(name="singleWorker") + self.busy = None + + def stopThread(self): + """Signal through the queue that the thread should be stopped""" + + try: + queues.workerQueue.put(("stopThread", "data")) + except queue.Full: + self.logger.error('workerQueue is Full') + super(singleWorker, self).stopThread() + + def run(self): + """To run single worker thread""" + if state.shutdown > 0: + return + + while state.shutdown == 0: + self.busy = 0 + command, _ = queues.workerQueue.get() + self.busy = 1 + if command == 'stopThread': + self.busy = 0 + return + + queues.workerQueue.task_done() + self.logger.info("Quitting...") diff --git a/src/tests/mock/pybitmessage/inventory.py b/src/tests/mock/pybitmessage/inventory.py new file mode 100644 index 00000000..62468e86 --- /dev/null +++ b/src/tests/mock/pybitmessage/inventory.py @@ -0,0 +1,15 @@ +"""The Inventory singleton""" + +# TODO make this dynamic, and watch out for frozen, like with messagetypes +from singleton import Singleton + + +# pylint: disable=old-style-class,too-few-public-methods +@Singleton +class Inventory(): + """ + Inventory singleton class which uses storage backends + to manage the inventory. + """ + def __init__(self): + self.numberOfInventoryLookupsPerformed = 0 diff --git a/src/tests/mock/pybitmessage/mpybit.py b/src/tests/mock/pybitmessage/mpybit.py new file mode 100644 index 00000000..b44b1070 --- /dev/null +++ b/src/tests/mock/pybitmessage/mpybit.py @@ -0,0 +1,28 @@ +""" + Dummy implementation for kivy Desktop and android(mobile) interface +""" +# pylint: disable=too-few-public-methods + +from kivy.app import App +from kivy.uix.label import Label + + +class NavigateApp(App): + """Navigation Layout of class""" + + def build(self): + """Method builds the widget""" + # pylint: disable=no-self-use + return Label(text="Hello World !") + + def clickNavDrawer(self): + """method for clicking navigation drawer""" + pass + + def addingtoaddressbook(self): + """method for clicking address book popup""" + pass + + +if __name__ == '__main__': + NavigateApp().run() diff --git a/src/tests/mock/pybitmessage/queues.py b/src/tests/mock/pybitmessage/queues.py new file mode 100644 index 00000000..8f5aea07 --- /dev/null +++ b/src/tests/mock/pybitmessage/queues.py @@ -0,0 +1,55 @@ +"""Most of the queues used by bitmessage threads are defined here.""" + +import threading +import time + +from six.moves import queue + +# try: +# from multiqueue import MultiQueue +# except ImportError: +# from .multiqueue import MultiQueue + + +class ObjectProcessorQueue(queue.Queue): + """Special queue class using lock for `.threads.objectProcessor`""" + + maxSize = 32000000 + + def __init__(self): + queue.Queue.__init__(self) + self.sizeLock = threading.Lock() + #: in Bytes. We maintain this to prevent nodes from flooding us + #: with objects which take up too much memory. If this gets + #: too big we'll sleep before asking for further objects. + self.curSize = 0 + + def put(self, item, block=True, timeout=None): + while self.curSize >= self.maxSize: + time.sleep(1) + with self.sizeLock: + self.curSize += len(item[1]) + queue.Queue.put(self, item, block, timeout) + + def get(self, block=True, timeout=None): + item = queue.Queue.get(self, block, timeout) + with self.sizeLock: + self.curSize -= len(item[1]) + return item + + +workerQueue = queue.Queue() +UISignalQueue = queue.Queue() +addressGeneratorQueue = queue.Queue() +#: `.network.ReceiveQueueThread` instances dump objects they hear +#: on the network into this queue to be processed. +objectProcessorQueue = ObjectProcessorQueue() +# invQueue = MultiQueue() +# addrQueue = MultiQueue() +portCheckerQueue = queue.Queue() +receiveDataQueue = queue.Queue() +#: The address generator thread uses this queue to get information back +#: to the API thread. +apiAddressGeneratorReturnQueue = queue.Queue() +#: for exceptions +excQueue = queue.Queue() diff --git a/src/tests/mock/pybitmessage/singleton.py b/src/tests/mock/pybitmessage/singleton.py new file mode 100644 index 00000000..5c6c43be --- /dev/null +++ b/src/tests/mock/pybitmessage/singleton.py @@ -0,0 +1,22 @@ +""" +Singleton decorator definition +""" + +from functools import wraps + + +def Singleton(cls): + """ + Decorator implementing the singleton pattern: + it restricts the instantiation of a class to one "single" instance. + """ + instances = {} + + # https://github.com/sphinx-doc/sphinx/issues/3783 + @wraps(cls) + def getinstance(): + """Find an instance or save newly created one""" + if cls not in instances: + instances[cls] = cls() + return instances[cls] + return getinstance diff --git a/src/tests/mock/pybitmessage/state.py b/src/tests/mock/pybitmessage/state.py new file mode 100644 index 00000000..be81992d --- /dev/null +++ b/src/tests/mock/pybitmessage/state.py @@ -0,0 +1,72 @@ +""" +Global runtime variables. +""" + +neededPubkeys = {} +streamsInWhichIAmParticipating = [] + +extPort = None +"""For UPnP""" + +socksIP = None +"""for Tor hidden service""" + +appdata = '' +"""holds the location of the application data storage directory""" + +shutdown = 0 +""" +Set to 1 by the `.shutdown.doCleanShutdown` function. +Used to tell the threads to exit. +""" + +# Component control flags - set on startup, do not change during runtime +# The defaults are for standalone GUI (default operating mode) +enableNetwork = True +"""enable network threads""" +enableObjProc = True +"""enable object processing thread""" +enableAPI = True +"""enable API (if configured)""" +enableGUI = True +"""enable GUI (QT or ncurses)""" +enableSTDIO = False +"""enable STDIO threads""" +enableKivy = False +"""enable kivy app and test cases""" +curses = False + +maximumNumberOfHalfOpenConnections = 0 + +maximumLengthOfTimeToBotherResendingMessages = 0 + +invThread = None +addrThread = None +downloadThread = None +uploadThread = None + +ownAddresses = {} + +discoveredPeers = {} + +dandelion = 0 + +testmode = False + +clientHasReceivedIncomingConnections = False +"""used by API command clientStatus""" + +numberOfMessagesProcessed = 0 +numberOfBroadcastsProcessed = 0 +numberOfPubkeysProcessed = 0 + +statusIconColor = 'red' +""" +GUI status icon color +.. note:: bad style, refactor it +""" + +ackdataForWhichImWatching = {} + +thisapp = None +"""Singleton instance""" diff --git a/src/tests/mock/pybitmessage/threads.py b/src/tests/mock/pybitmessage/threads.py new file mode 100644 index 00000000..336aae5f --- /dev/null +++ b/src/tests/mock/pybitmessage/threads.py @@ -0,0 +1,33 @@ +"""Threading primitives for the network package""" + +import logging +import random +import threading + + +class StoppableThread(threading.Thread): + """Base class for application threads with stopThread method""" + name = None + logger = logging.getLogger('default') + + def __init__(self, name=None): + if name: + self.name = name + super(StoppableThread, self).__init__(name=self.name) + self.stop = threading.Event() + self._stopped = False + random.seed() + self.logger.info('Init thread %s', self.name) + + def stopThread(self): + """Stop the thread""" + self._stopped = True + self.stop.set() + + +class BusyError(threading.ThreadError): + """ + Thread error raised when another connection holds the lock + we are trying to acquire. + """ + pass