summaryrefslogtreecommitdiffstats
path: root/deluge/tests/common.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/tests/common.py')
-rw-r--r--deluge/tests/common.py363
1 files changed, 363 insertions, 0 deletions
diff --git a/deluge/tests/common.py b/deluge/tests/common.py
new file mode 100644
index 0000000..b594156
--- /dev/null
+++ b/deluge/tests/common.py
@@ -0,0 +1,363 @@
+#
+# Copyright (C) 2016 bendikro <bro.devel+deluge@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import os
+import sys
+import traceback
+
+import pytest
+from twisted.internet import defer, protocol, reactor
+from twisted.internet.defer import Deferred
+from twisted.internet.error import CannotListenError
+
+import deluge.configmanager
+import deluge.core.preferencesmanager
+import deluge.log
+from deluge.common import get_localhost_auth
+from deluge.error import DelugeError
+from deluge.ui.client import Client
+
+# This sets log level to critical, so use log.critical() to debug while running unit tests
+deluge.log.setup_logger('none')
+
+
+def disable_new_release_check():
+ deluge.core.preferencesmanager.DEFAULT_PREFS['new_release_check'] = False
+
+
+def setup_test_logger(level='info', prefix='deluge'):
+ deluge.log.setup_logger(level, filename='%s.log' % prefix, twisted_observer=False)
+
+
+def get_test_data_file(filename):
+ return os.path.join(os.path.join(os.path.dirname(__file__), 'data'), filename)
+
+
+def todo_test(caller):
+ # If we are using the delugereporter we can set todo mark on the test
+ # Without the delugereporter the todo would print a stack trace, so in
+ # that case we rely only on skipTest
+ if os.environ.get('DELUGE_REPORTER', None):
+ getattr(caller, caller._testMethodName).__func__.todo = 'To be fixed'
+
+ filename = os.path.basename(traceback.extract_stack(None, 2)[0][0])
+ funcname = traceback.extract_stack(None, 2)[0][2]
+ pytest.skip(f'TODO: {filename}:{funcname}')
+
+
+def add_watchdog(deferred, timeout=0.05, message=None):
+ def callback(value):
+ if not watchdog.called and not watchdog.cancelled:
+ watchdog.cancel()
+ if not deferred.called:
+ if message:
+ print(message)
+ deferred.cancel()
+ return value
+
+ deferred.addBoth(callback)
+ watchdog = reactor.callLater(timeout, defer.Deferred.addTimeout, deferred)
+ return watchdog
+
+
+class ReactorOverride:
+ """Class used to patch reactor while running unit tests
+ to avoid starting and stopping the twisted reactor
+ """
+
+ def __getattr__(self, attr):
+ if attr == 'run':
+ return self._run
+ if attr == 'stop':
+ return self._stop
+ return getattr(reactor, attr)
+
+ def _run(self):
+ pass
+
+ def _stop(self):
+ pass
+
+ def addReader(self, arg): # NOQA: N802
+ pass
+
+
+class ProcessOutputHandler(protocol.ProcessProtocol):
+ def __init__(
+ self,
+ script,
+ shutdown_func,
+ callbacks,
+ logfile=None,
+ print_stdout=True,
+ print_stderr=True,
+ ):
+ """Executes a script and handle the process' output to stdout and stderr.
+
+ Args:
+ script (str): The script to execute.
+ shutdown_func (func): A function which will gracefully stop the called script.
+ callbacks (list): Callbacks to trigger if the expected output if found.
+ logfile (str, optional): Filename to wrote the process' output.
+ print_stderr (bool): Print the process' stderr output to stdout.
+ print_stdout (bool): Print the process' stdout output to stdout.
+
+ """
+ self.callbacks = callbacks
+ self.script = script
+ self.shutdown_func = shutdown_func
+ self.log_output = ''
+ self.stderr_out = ''
+ self.logfile = logfile
+ self.print_stdout = print_stdout
+ self.print_stderr = print_stderr
+ self.quit_d = None
+ self.killed = False
+ self.watchdogs = []
+
+ def connectionMade(self): # NOQA: N802
+ self.transport.write(self.script)
+ self.transport.closeStdin()
+
+ def outConnectionLost(self): # NOQA: N802
+ if not self.logfile:
+ return
+ with open(self.logfile, 'w') as f:
+ f.write(self.log_output)
+
+ @defer.inlineCallbacks
+ def kill(self):
+ """Kill the running process.
+
+ Returns:
+ Deferred: A deferred that is triggered when the process has quit.
+
+ """
+ if self.killed:
+ return
+ self.killed = True
+ self._kill_watchdogs()
+ self.quit_d = Deferred()
+ shutdown = self.shutdown_func()
+ shutdown.addTimeout(5, reactor)
+ try:
+ yield shutdown
+ except Exception:
+ self.transport.signalProcess('TERM')
+ result = yield self.quit_d
+ return result
+
+ def _kill_watchdogs(self):
+ """Cancel all watchdogs"""
+ for w in self.watchdogs:
+ if not w.called and not w.cancelled:
+ w.cancel()
+
+ def processEnded(self, status): # NOQA: N802
+ self.transport.loseConnection()
+ if self.quit_d is None:
+ return
+ if status.value.exitCode == 0:
+ self.quit_d.callback(True)
+ else:
+ self.quit_d.errback(status)
+
+ def check_callbacks(self, data, cb_type='stdout'):
+ ret = False
+ for c in self.callbacks:
+ if cb_type not in c['types'] or c['deferred'].called:
+ continue
+ for trigger in c['triggers']:
+ if trigger['expr'] in data:
+ ret = True
+ if 'cb' in trigger:
+ trigger['cb'](self, c['deferred'], data, self.log_output)
+ elif 'value' not in trigger:
+ raise Exception('Trigger must specify either "cb" or "value"')
+ else:
+ val = trigger['value'](self, data, self.log_output)
+ if trigger.get('type', 'callback') == 'errback':
+ c['deferred'].errback(val)
+ else:
+ c['deferred'].callback(val)
+ return ret
+
+ def outReceived(self, data): # NOQA: N802
+ """Process output from stdout"""
+ data = data.decode('utf8')
+ self.log_output += data
+ if self.check_callbacks(data):
+ pass
+ elif '[ERROR' in data:
+ if not self.print_stdout:
+ return
+ print(data, end=' ')
+
+ def errReceived(self, data): # NOQA: N802
+ """Process output from stderr"""
+ data = data.decode('utf8')
+ self.log_output += data
+ self.stderr_out += data
+ self.check_callbacks(data, cb_type='stderr')
+ if not self.print_stderr:
+ return
+ data = '\n%s' % data.strip()
+ prefixed = data.replace('\n', '\nSTDERR: ')
+ print('\n%s' % prefixed)
+
+
+def start_core(
+ listen_port=58900,
+ logfile=None,
+ timeout=10,
+ timeout_msg=None,
+ custom_script='',
+ print_stdout=True,
+ print_stderr=True,
+ extra_callbacks=None,
+ config_directory='',
+):
+ """Start the deluge core as a daemon.
+
+ Args:
+ listen_port (int, optional): The port the daemon listens for client connections.
+ logfile (str, optional): Logfile name to write the output from the process.
+ timeout (int): If none of the callbacks have been triggered before the timeout, the process is killed.
+ timeout_msg (str): The message to print when the timeout expires.
+ custom_script (str): Extra python code to insert into the daemon process script.
+ print_stderr (bool): If the output from the process' stderr should be printed to stdout.
+ print_stdout (bool): If the output from the process' stdout should be printed to stdout.
+ extra_callbacks (list): A list of dictionaries specifying extra callbacks.
+
+ Returns:
+ tuple(Deferred, ProcessOutputHandler):
+
+ The Deferred is fired when the core callback is triggered either after the default
+ output triggers are matched (daemon successfully started, or failed to start),
+ or upon timeout expiry. The ProcessOutputHandler is the handler for the deluged process.
+
+ """
+ daemon_script = """
+import sys
+import deluge.core.daemon_entry
+
+from deluge.common import windows_check
+
+if windows_check():
+ sys.argv.extend(['-c', '%(dir)s', '-L', 'info', '-p', '%(port)d'])
+else:
+ sys.argv.extend(['-d', '-c', '%(dir)s', '-L', 'info', '-p', '%(port)d'])
+
+try:
+ daemon = deluge.core.daemon_entry.start_daemon(skip_start=True)
+ %(script)s
+ daemon.start()
+except Exception:
+ import traceback
+ sys.stderr.write('Exception raised:\\n %%s' %% traceback.format_exc())
+""" % {
+ 'dir': config_directory.as_posix(),
+ 'port': listen_port,
+ 'script': custom_script,
+ }
+
+ callbacks = []
+ default_core_cb = {'deferred': Deferred(), 'types': 'stdout'}
+ if timeout:
+ default_core_cb['timeout'] = timeout
+
+ # Specify the triggers for daemon log output
+ default_core_cb['triggers'] = [
+ {'expr': 'Finished loading ', 'value': lambda reader, data, data_all: reader},
+ {
+ 'expr': 'Cannot start deluged, listen port in use.',
+ 'type': 'errback',
+ 'value': lambda reader, data, data_all: CannotListenError(
+ 'localhost',
+ listen_port,
+ 'Could not start deluge test client!\n%s' % data,
+ ),
+ },
+ {
+ 'expr': 'Traceback',
+ 'type': 'errback',
+ 'value': lambda reader, data, data_all: DelugeError(
+ 'Traceback found when starting daemon:\n%s' % data
+ ),
+ },
+ ]
+
+ callbacks.append(default_core_cb)
+ if extra_callbacks:
+ callbacks.extend(extra_callbacks)
+
+ @defer.inlineCallbacks
+ def shutdown_daemon():
+ username, password = get_localhost_auth()
+ client = Client()
+ yield client.connect(
+ 'localhost', listen_port, username=username, password=password
+ )
+ yield client.daemon.shutdown()
+
+ process_protocol = start_process(
+ daemon_script, shutdown_daemon, callbacks, logfile, print_stdout, print_stderr
+ )
+ return default_core_cb['deferred'], process_protocol
+
+
+def start_process(
+ script, shutdown_func, callbacks, logfile=None, print_stdout=True, print_stderr=True
+):
+ """
+ Starts an external python process which executes the given script.
+
+ Args:
+ script (str): The content of the script to execute.
+ shutdown_func (func): A function which will gracefully end the called script.
+ callbacks (list): list of dictionaries specifying callbacks.
+ logfile (str, optional): Logfile name to write the output from the process.
+ print_stderr (bool): If the output from the process' stderr should be printed to stdout.
+ print_stdout (bool): If the output from the process' stdout should be printed to stdout.
+
+ Returns:
+ ProcessOutputHandler: The handler for the process's output.
+
+ Each entry in the callbacks list is a dictionary with the following keys:
+ * "deferred": The deferred to be called when matched.
+ * "types": The output this callback should be matched against.
+ Possible values: ["stdout", "stderr"]
+ * "timeout" (optional): A timeout in seconds for the deferred.
+ * "triggers": A list of dictionaries, each specifying specifying a trigger:
+ * "expr": A string to match against the log output.
+ * "value": A function to produce the result to be passed to the callback.
+ * "type" (optional): A string that specifies wether to trigger a regular callback or errback.
+
+ """
+ cwd = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+ process_protocol = ProcessOutputHandler(
+ script.encode('utf8'),
+ shutdown_func,
+ callbacks,
+ logfile,
+ print_stdout,
+ print_stderr,
+ )
+
+ # Add timeouts to deferreds
+ for c in callbacks:
+ if 'timeout' in c:
+ w = add_watchdog(
+ c['deferred'], timeout=c['timeout'], message=c.get('timeout_msg', None)
+ )
+ process_protocol.watchdogs.append(w)
+
+ reactor.spawnProcess(
+ process_protocol, sys.executable, args=[sys.executable], path=cwd
+ )
+ return process_protocol