2015-10-10 17:20:20 +02:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
2016-01-04 07:12:39 +01:00
|
|
|
# Copyright 2015-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
2015-10-10 17:20:20 +02:00
|
|
|
#
|
|
|
|
# This file is part of qutebrowser.
|
|
|
|
#
|
|
|
|
# qutebrowser is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# qutebrowser is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
"""Base class for a subprocess run for tests.."""
|
|
|
|
|
2015-10-22 22:23:55 +02:00
|
|
|
import re
|
2015-11-10 09:24:47 +01:00
|
|
|
import os
|
2015-11-09 07:43:48 +01:00
|
|
|
import time
|
2015-10-22 22:23:55 +02:00
|
|
|
|
2015-12-16 20:17:29 +01:00
|
|
|
import pytest
|
2015-12-01 20:58:08 +01:00
|
|
|
import pytestqt.plugin
|
2015-10-22 22:23:55 +02:00
|
|
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer
|
|
|
|
from PyQt5.QtTest import QSignalSpy
|
2015-10-10 17:20:20 +02:00
|
|
|
|
2015-12-01 22:47:10 +01:00
|
|
|
from helpers import utils
|
2015-11-13 07:49:33 +01:00
|
|
|
|
2015-10-10 17:20:20 +02:00
|
|
|
|
|
|
|
class InvalidLine(Exception):
|
|
|
|
|
|
|
|
"""Raised when the process prints a line which is not parsable."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2015-10-10 18:17:07 +02:00
|
|
|
class ProcessExited(Exception):
|
|
|
|
|
|
|
|
"""Raised when the child process did exit."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2015-10-22 22:23:55 +02:00
|
|
|
class WaitForTimeout(Exception):
|
|
|
|
|
|
|
|
"""Raised when wait_for didn't get the expected message."""
|
|
|
|
|
|
|
|
|
2015-11-13 23:27:33 +01:00
|
|
|
class BlacklistedMessageError(Exception):
|
|
|
|
|
|
|
|
"""Raised when ensure_not_logged found a message."""
|
|
|
|
|
|
|
|
|
2015-11-06 06:49:36 +01:00
|
|
|
class Line:
|
|
|
|
|
|
|
|
"""Container for a line of data the process emits.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
data: The raw data passed to the constructor.
|
tests: Don't wait for the same line twice.
We need to search for lines in the history because we could miss something
otherwise, but for subsequent wait_for calls, we really don't want to wait for
the same thing again.
This should make test_backforward.py more stable as it *actually* waits when
going back now. Before, it did produce failures such as this one on OS X:
____________________________ test_going_backforward ____________________________
[..]
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
def list_of_loaded_pages(httpbin, pages):
requests = [httpbin.Request('GET', '/' + path.strip())
for path in pages.split('\n')]
> assert httpbin.get_requests() == requests
E assert [Request(verb...rward/1.txt')] == [Request(verb=...rward/2.txt')]
E At index 3 diff: Request(verb='GET', path='/data/backforward/1.txt') != Request(verb='GET', path='/data/backforward/2.txt')
E Full diff:
E [Request(verb='GET', path='/data/backforward/1.txt'),
E Request(verb='GET', path='/data/backforward/2.txt'),
E Request(verb='GET', path='/data/backforward/1.txt'),
E - Request(verb='GET', path='/data/backforward/1.txt')]
E ? ^
E + Request(verb='GET', path='/data/backforward/2.txt')]
E ? ^
tests/integration/features/conftest.py:85: AssertionError
2015-11-06 06:57:38 +01:00
|
|
|
waited_for: If Process.wait_for was used on this line already.
|
2015-11-06 06:49:36 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, data):
|
|
|
|
self.data = data
|
tests: Don't wait for the same line twice.
We need to search for lines in the history because we could miss something
otherwise, but for subsequent wait_for calls, we really don't want to wait for
the same thing again.
This should make test_backforward.py more stable as it *actually* waits when
going back now. Before, it did produce failures such as this one on OS X:
____________________________ test_going_backforward ____________________________
[..]
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
def list_of_loaded_pages(httpbin, pages):
requests = [httpbin.Request('GET', '/' + path.strip())
for path in pages.split('\n')]
> assert httpbin.get_requests() == requests
E assert [Request(verb...rward/1.txt')] == [Request(verb=...rward/2.txt')]
E At index 3 diff: Request(verb='GET', path='/data/backforward/1.txt') != Request(verb='GET', path='/data/backforward/2.txt')
E Full diff:
E [Request(verb='GET', path='/data/backforward/1.txt'),
E Request(verb='GET', path='/data/backforward/2.txt'),
E Request(verb='GET', path='/data/backforward/1.txt'),
E - Request(verb='GET', path='/data/backforward/1.txt')]
E ? ^
E + Request(verb='GET', path='/data/backforward/2.txt')]
E ? ^
tests/integration/features/conftest.py:85: AssertionError
2015-11-06 06:57:38 +01:00
|
|
|
self.waited_for = False
|
2015-11-06 06:49:36 +01:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return '{}({!r})'.format(self.__class__.__name__, self.data)
|
|
|
|
|
|
|
|
|
2016-01-07 19:39:46 +01:00
|
|
|
def _render_log(data, threshold=50):
|
|
|
|
"""Shorten the given log without -v and convert to a string."""
|
|
|
|
# pylint: disable=no-member
|
|
|
|
if len(data) > threshold and not pytest.config.getoption('--verbose'):
|
|
|
|
msg = '[{} lines suppressed, use -v to show]'.format(
|
|
|
|
len(data) - threshold)
|
|
|
|
data = [msg] + data[-threshold:]
|
|
|
|
return '\n'.join(data)
|
|
|
|
|
|
|
|
|
2015-12-16 20:17:29 +01:00
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
|
|
def pytest_runtest_makereport(item, call):
|
|
|
|
"""Add qutebrowser/httpbin sections to captured output if a test failed."""
|
|
|
|
outcome = yield
|
|
|
|
if call.when != 'call':
|
|
|
|
return
|
|
|
|
report = outcome.get_result()
|
|
|
|
|
|
|
|
if report.passed:
|
|
|
|
return
|
|
|
|
|
|
|
|
quteproc_log = getattr(item, '_quteproc_log', None)
|
|
|
|
httpbin_log = getattr(item, '_httpbin_log', None)
|
|
|
|
|
2015-12-16 22:47:33 +01:00
|
|
|
if not hasattr(report.longrepr, 'addsection'):
|
|
|
|
# In some conditions (on OS X and Windows it seems), report.longrepr is
|
|
|
|
# actually a tuple. This is handled similarily in pytest-qt too.
|
|
|
|
return
|
|
|
|
|
2016-01-08 01:16:58 +01:00
|
|
|
# pylint: disable=no-member
|
2016-01-07 21:15:24 +01:00
|
|
|
if pytest.config.getoption('--capture') == 'no':
|
|
|
|
# Already printed live
|
|
|
|
return
|
|
|
|
|
2015-12-16 20:17:29 +01:00
|
|
|
if quteproc_log is not None:
|
|
|
|
report.longrepr.addsection("qutebrowser output",
|
2016-01-07 19:39:46 +01:00
|
|
|
_render_log(quteproc_log))
|
2015-12-16 20:17:29 +01:00
|
|
|
if httpbin_log is not None:
|
2016-01-07 19:39:46 +01:00
|
|
|
report.longrepr.addsection("httpbin output", _render_log(httpbin_log))
|
2015-12-16 20:17:29 +01:00
|
|
|
|
|
|
|
|
2015-10-10 17:20:20 +02:00
|
|
|
class Process(QObject):
|
|
|
|
|
|
|
|
"""Abstraction over a running test subprocess process.
|
|
|
|
|
|
|
|
Reads the log from its stdout and parses it.
|
|
|
|
|
2015-11-26 14:25:33 +01:00
|
|
|
Attributes:
|
|
|
|
_invalid: A list of lines which could not be parsed.
|
|
|
|
_data: A list of parsed lines.
|
|
|
|
proc: The QProcess for the underlying process.
|
|
|
|
|
2015-10-10 17:20:20 +02:00
|
|
|
Signals:
|
2015-10-10 19:21:12 +02:00
|
|
|
ready: Emitted when the server finished starting up.
|
2015-10-10 17:20:20 +02:00
|
|
|
new_data: Emitted when a new line was parsed.
|
|
|
|
"""
|
|
|
|
|
2015-10-10 19:21:12 +02:00
|
|
|
ready = pyqtSignal()
|
2015-10-10 17:20:20 +02:00
|
|
|
new_data = pyqtSignal(object)
|
2015-11-13 23:26:14 +01:00
|
|
|
KEYS = ['data']
|
2015-10-10 17:20:20 +02:00
|
|
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
super().__init__(parent)
|
2015-12-16 20:17:29 +01:00
|
|
|
self.captured_log = []
|
2015-11-09 07:41:57 +01:00
|
|
|
self._invalid = []
|
2015-10-10 17:20:20 +02:00
|
|
|
self._data = []
|
|
|
|
self.proc = QProcess()
|
|
|
|
self.proc.setReadChannel(QProcess.StandardError)
|
|
|
|
|
2015-12-16 20:17:29 +01:00
|
|
|
def _log(self, line):
|
|
|
|
"""Add the given line to the captured log output."""
|
2016-01-08 01:16:58 +01:00
|
|
|
# pylint: disable=no-member
|
2016-01-07 21:15:24 +01:00
|
|
|
if pytest.config.getoption('--capture') == 'no':
|
|
|
|
print(line)
|
2015-12-16 20:17:29 +01:00
|
|
|
self.captured_log.append(line)
|
|
|
|
|
2015-10-10 17:20:20 +02:00
|
|
|
def _parse_line(self, line):
|
|
|
|
"""Parse the given line from the log.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
A self.ParseResult member.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def _executable_args(self):
|
2015-10-10 18:16:47 +02:00
|
|
|
"""Get the executable and arguments to pass to it as a tuple."""
|
2015-10-10 17:20:20 +02:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def _get_data(self):
|
|
|
|
"""Get the parsed data for this test.
|
|
|
|
|
|
|
|
Also waits for 0.5s to make sure any new data is received.
|
|
|
|
|
|
|
|
Subprocesses are expected to alias this to a public method with a
|
|
|
|
better name.
|
|
|
|
"""
|
|
|
|
self.proc.waitForReadyRead(500)
|
|
|
|
self.read_log()
|
|
|
|
return self._data
|
|
|
|
|
2015-10-10 19:21:12 +02:00
|
|
|
def _wait_signal(self, signal, timeout=5000, raising=True):
|
|
|
|
"""Wait for a signal to be emitted.
|
|
|
|
|
|
|
|
Should be used in a contextmanager.
|
|
|
|
"""
|
|
|
|
blocker = pytestqt.plugin.SignalBlocker(
|
|
|
|
timeout=timeout, raising=raising)
|
|
|
|
blocker.connect(signal)
|
|
|
|
return blocker
|
|
|
|
|
2015-10-10 17:20:20 +02:00
|
|
|
@pyqtSlot()
|
|
|
|
def read_log(self):
|
|
|
|
"""Read the log from the process' stdout."""
|
2015-10-27 07:08:17 +01:00
|
|
|
if not hasattr(self, 'proc'):
|
|
|
|
# I have no idea how this happens, but it does...
|
|
|
|
return
|
2015-10-10 17:20:20 +02:00
|
|
|
while self.proc.canReadLine():
|
|
|
|
line = self.proc.readLine()
|
2015-10-14 20:48:12 +02:00
|
|
|
line = bytes(line).decode('utf-8', errors='ignore').rstrip('\r\n')
|
2015-10-10 17:20:20 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
parsed = self._parse_line(line)
|
|
|
|
except InvalidLine:
|
2015-11-09 07:41:57 +01:00
|
|
|
self._invalid.append(line)
|
2015-12-16 20:17:29 +01:00
|
|
|
self._log("INVALID: {}".format(line))
|
2015-10-10 17:20:20 +02:00
|
|
|
continue
|
|
|
|
|
2015-11-10 08:47:29 +01:00
|
|
|
if parsed is None:
|
2015-11-12 22:13:56 +01:00
|
|
|
if self._invalid:
|
2015-12-16 20:17:29 +01:00
|
|
|
self._log("IGNORED: {}".format(line))
|
2015-11-10 08:47:29 +01:00
|
|
|
else:
|
2015-10-10 17:20:20 +02:00
|
|
|
self._data.append(parsed)
|
|
|
|
self.new_data.emit(parsed)
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
"""Start the process and wait until it started."""
|
2015-11-03 06:35:52 +01:00
|
|
|
with self._wait_signal(self.ready, timeout=60000):
|
2015-10-10 17:20:20 +02:00
|
|
|
self._start()
|
|
|
|
|
|
|
|
def _start(self):
|
|
|
|
"""Actually start the process."""
|
2015-10-10 18:16:47 +02:00
|
|
|
executable, args = self._executable_args()
|
2015-10-28 07:12:37 +01:00
|
|
|
self.proc.readyRead.connect(self.read_log)
|
2015-10-10 18:16:47 +02:00
|
|
|
self.proc.start(executable, args)
|
2015-10-10 17:20:20 +02:00
|
|
|
ok = self.proc.waitForStarted()
|
|
|
|
assert ok
|
2015-10-10 19:21:12 +02:00
|
|
|
assert self.is_running()
|
2015-10-10 17:20:20 +02:00
|
|
|
|
2015-11-09 07:43:48 +01:00
|
|
|
def before_test(self):
|
|
|
|
"""Restart process before a test if it exited before."""
|
|
|
|
self._invalid = []
|
|
|
|
if not self.is_running():
|
|
|
|
self.start()
|
|
|
|
|
2015-10-10 17:20:20 +02:00
|
|
|
def after_test(self):
|
|
|
|
"""Clean up data after each test.
|
|
|
|
|
|
|
|
Also checks self._invalid so the test counts as failed if there were
|
|
|
|
unexpected output lines earlier.
|
|
|
|
"""
|
2015-12-16 23:07:42 +01:00
|
|
|
self.captured_log = []
|
2015-10-10 17:20:20 +02:00
|
|
|
if self._invalid:
|
2015-11-09 07:43:48 +01:00
|
|
|
# Wait for a bit so the full error has a chance to arrive
|
|
|
|
time.sleep(1)
|
|
|
|
# Exit the process to make sure we're in a defined state again
|
|
|
|
self.terminate()
|
2015-11-21 00:10:49 +01:00
|
|
|
self.clear_data()
|
2015-11-09 07:41:57 +01:00
|
|
|
raise InvalidLine(self._invalid)
|
2015-11-09 07:43:48 +01:00
|
|
|
|
2015-11-21 00:10:49 +01:00
|
|
|
self.clear_data()
|
2015-11-03 07:00:46 +01:00
|
|
|
if not self.is_running():
|
|
|
|
raise ProcessExited
|
2015-10-10 17:20:20 +02:00
|
|
|
|
2015-11-21 00:10:49 +01:00
|
|
|
def clear_data(self):
|
|
|
|
"""Clear the collected data."""
|
|
|
|
self._data.clear()
|
|
|
|
|
2015-10-27 07:11:23 +01:00
|
|
|
def terminate(self):
|
2015-10-10 17:20:20 +02:00
|
|
|
"""Clean up and shut down the process."""
|
|
|
|
self.proc.terminate()
|
|
|
|
self.proc.waitForFinished()
|
2015-10-10 19:21:12 +02:00
|
|
|
|
|
|
|
def is_running(self):
|
|
|
|
"""Check if the process is currently running."""
|
|
|
|
return self.proc.state() == QProcess.Running
|
2015-10-22 22:23:55 +02:00
|
|
|
|
2015-11-05 06:37:35 +01:00
|
|
|
def _match_data(self, value, expected):
|
|
|
|
"""Helper for wait_for to match a given value.
|
2015-10-22 22:23:55 +02:00
|
|
|
|
2015-11-01 22:10:48 +01:00
|
|
|
The behavior of this method is slightly different depending on the
|
|
|
|
types of the filtered values:
|
|
|
|
|
2015-11-05 06:37:35 +01:00
|
|
|
- If expected is None, the filter always matches.
|
2015-11-01 22:10:48 +01:00
|
|
|
- If the value is a string or bytes object and the expected value is
|
2015-11-13 07:49:33 +01:00
|
|
|
too, the pattern is treated as a glob pattern (with only * active).
|
2015-11-01 22:10:48 +01:00
|
|
|
- If the value is a string or bytes object and the expected value is a
|
|
|
|
compiled regex, it is used for matching.
|
|
|
|
- If the value is any other type, == is used.
|
|
|
|
|
2015-11-05 06:37:35 +01:00
|
|
|
Return:
|
|
|
|
A bool
|
|
|
|
"""
|
|
|
|
regex_type = type(re.compile(''))
|
|
|
|
if expected is None:
|
|
|
|
return True
|
|
|
|
elif isinstance(expected, regex_type):
|
|
|
|
return expected.match(value)
|
|
|
|
elif isinstance(value, (bytes, str)):
|
2015-11-13 07:49:33 +01:00
|
|
|
return utils.pattern_match(pattern=expected, value=value)
|
2015-11-05 06:37:35 +01:00
|
|
|
else:
|
|
|
|
return value == expected
|
|
|
|
|
2015-11-29 01:02:21 +01:00
|
|
|
def _wait_for_existing(self, override_waited_for, **kwargs):
|
|
|
|
"""Check if there are any line in the history for wait_for.
|
|
|
|
|
|
|
|
Return: either the found line or None.
|
|
|
|
"""
|
|
|
|
for line in self._data:
|
|
|
|
matches = []
|
|
|
|
|
|
|
|
for key, expected in kwargs.items():
|
|
|
|
value = getattr(line, key)
|
|
|
|
matches.append(self._match_data(value, expected))
|
|
|
|
|
|
|
|
if all(matches) and (not line.waited_for or override_waited_for):
|
|
|
|
# If we waited for this line, chances are we don't mean the
|
|
|
|
# same thing the next time we use wait_for and it matches
|
|
|
|
# this line again.
|
|
|
|
line.waited_for = True
|
|
|
|
return line
|
|
|
|
return None
|
|
|
|
|
2016-01-06 08:49:30 +01:00
|
|
|
def _wait_for_match(self, spy, kwargs):
|
|
|
|
"""Try matching the kwargs with the given QSignalSpy."""
|
|
|
|
for args in spy:
|
|
|
|
assert len(args) == 1
|
|
|
|
line = args[0]
|
|
|
|
|
|
|
|
matches = []
|
|
|
|
|
|
|
|
for key, expected in kwargs.items():
|
|
|
|
value = getattr(line, key)
|
|
|
|
matches.append(self._match_data(value, expected))
|
|
|
|
|
|
|
|
if all(matches):
|
|
|
|
# If we waited for this line, chances are we don't mean the
|
|
|
|
# same thing the next time we use wait_for and it matches
|
|
|
|
# this line again.
|
|
|
|
line.waited_for = True
|
|
|
|
return line
|
|
|
|
return None
|
|
|
|
|
2016-01-06 08:22:30 +01:00
|
|
|
def wait_for(self, timeout=None, *, override_waited_for=False,
|
|
|
|
do_skip=False, **kwargs):
|
2015-11-05 06:37:35 +01:00
|
|
|
"""Wait until a given value is found in the data.
|
|
|
|
|
|
|
|
Keyword arguments to this function get interpreted as attributes of the
|
|
|
|
searched data. Every given argument is treated as a pattern which
|
|
|
|
the attribute has to match against.
|
|
|
|
|
2015-11-13 23:27:33 +01:00
|
|
|
Args:
|
|
|
|
timeout: How long to wait for the message.
|
|
|
|
override_waited_for: If set, gets triggered by previous messages
|
|
|
|
again.
|
2016-01-06 08:22:30 +01:00
|
|
|
do_skip: If set, call pytest.skip on a timeout.
|
2015-11-13 23:27:33 +01:00
|
|
|
|
2015-11-01 22:10:48 +01:00
|
|
|
Return:
|
|
|
|
The matched line.
|
2015-10-22 22:23:55 +02:00
|
|
|
"""
|
2015-11-13 06:45:55 +01:00
|
|
|
__tracebackhide__ = True
|
2015-11-10 09:24:47 +01:00
|
|
|
if timeout is None:
|
2016-01-06 08:22:30 +01:00
|
|
|
if do_skip:
|
|
|
|
timeout = 2000
|
|
|
|
elif 'CI' in os.environ:
|
2015-11-10 09:24:47 +01:00
|
|
|
timeout = 15000
|
|
|
|
else:
|
|
|
|
timeout = 5000
|
2015-11-28 23:36:26 +01:00
|
|
|
if not kwargs:
|
|
|
|
raise TypeError("No keyword arguments given!")
|
2015-11-13 23:26:14 +01:00
|
|
|
for key in kwargs:
|
|
|
|
assert key in self.KEYS
|
|
|
|
|
2015-11-05 06:37:35 +01:00
|
|
|
# Search existing messages
|
2015-11-29 01:02:21 +01:00
|
|
|
existing = self._wait_for_existing(override_waited_for, **kwargs)
|
|
|
|
if existing is not None:
|
|
|
|
return existing
|
2015-10-22 22:23:55 +02:00
|
|
|
|
2015-11-05 06:37:35 +01:00
|
|
|
# If there is none, wait for the message
|
2015-10-22 22:23:55 +02:00
|
|
|
spy = QSignalSpy(self.new_data)
|
|
|
|
elapsed_timer = QElapsedTimer()
|
|
|
|
elapsed_timer.start()
|
|
|
|
|
|
|
|
while True:
|
|
|
|
got_signal = spy.wait(timeout)
|
|
|
|
if not got_signal or elapsed_timer.hasExpired(timeout):
|
2016-01-06 08:22:30 +01:00
|
|
|
msg = "Timed out after {}ms waiting for {!r}.".format(
|
|
|
|
timeout, kwargs)
|
|
|
|
if do_skip:
|
|
|
|
pytest.skip(msg)
|
|
|
|
else:
|
|
|
|
raise WaitForTimeout(msg)
|
2015-10-22 22:23:55 +02:00
|
|
|
|
2016-01-06 08:49:30 +01:00
|
|
|
match = self._wait_for_match(spy, kwargs)
|
|
|
|
if match is not None:
|
|
|
|
return match
|
2015-11-13 23:27:33 +01:00
|
|
|
|
|
|
|
def ensure_not_logged(self, delay=500, **kwargs):
|
|
|
|
"""Make sure the data matching the given arguments is not logged.
|
|
|
|
|
|
|
|
If nothing is found in the log, we wait for delay ms to make sure
|
|
|
|
nothing arrives.
|
|
|
|
"""
|
|
|
|
__tracebackhide__ = True
|
|
|
|
try:
|
|
|
|
line = self.wait_for(timeout=delay, override_waited_for=True,
|
|
|
|
**kwargs)
|
|
|
|
except WaitForTimeout:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
raise BlacklistedMessageError(line)
|