mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 21:03:21 -07:00
Update cherrypy-18.6.1
This commit is contained in:
parent
b3ae6bd695
commit
ebffd124f6
57 changed files with 1269 additions and 1509 deletions
|
@ -1,274 +1,327 @@
|
|||
"""Publish-subscribe bus tests."""
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
|
||||
from cherrypy.process import wspbus
|
||||
|
||||
|
||||
msg = 'Listener %d on channel %s: %s.'
|
||||
CI_ON_MACOS = bool(os.getenv('CI')) and sys.platform == 'darwin'
|
||||
msg = 'Listener %d on channel %s: %s.' # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class PublishSubscribeTests(unittest.TestCase):
|
||||
|
||||
def get_listener(self, channel, index):
|
||||
def listener(arg=None):
|
||||
self.responses.append(msg % (index, channel, arg))
|
||||
return listener
|
||||
|
||||
def test_builtin_channels(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
self.responses, expected = [], []
|
||||
|
||||
for channel in b.listeners:
|
||||
for index, priority in enumerate([100, 50, 0, 51]):
|
||||
b.subscribe(channel,
|
||||
self.get_listener(channel, index), priority)
|
||||
|
||||
for channel in b.listeners:
|
||||
b.publish(channel)
|
||||
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
|
||||
b.publish(channel, arg=79347)
|
||||
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
|
||||
|
||||
self.assertEqual(self.responses, expected)
|
||||
|
||||
def test_custom_channels(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
self.responses, expected = [], []
|
||||
|
||||
custom_listeners = ('hugh', 'louis', 'dewey')
|
||||
for channel in custom_listeners:
|
||||
for index, priority in enumerate([None, 10, 60, 40]):
|
||||
b.subscribe(channel,
|
||||
self.get_listener(channel, index), priority)
|
||||
|
||||
for channel in custom_listeners:
|
||||
b.publish(channel, 'ah so')
|
||||
expected.extend([msg % (i, channel, 'ah so')
|
||||
for i in (1, 3, 0, 2)])
|
||||
b.publish(channel)
|
||||
expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
|
||||
|
||||
self.assertEqual(self.responses, expected)
|
||||
|
||||
def test_listener_errors(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
self.responses, expected = [], []
|
||||
channels = [c for c in b.listeners if c != 'log']
|
||||
|
||||
for channel in channels:
|
||||
b.subscribe(channel, self.get_listener(channel, 1))
|
||||
# This will break since the lambda takes no args.
|
||||
b.subscribe(channel, lambda: None, priority=20)
|
||||
|
||||
for channel in channels:
|
||||
self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
|
||||
expected.append(msg % (1, channel, 123))
|
||||
|
||||
self.assertEqual(self.responses, expected)
|
||||
@pytest.fixture
|
||||
def bus():
|
||||
"""Return a wspbus instance."""
|
||||
return wspbus.Bus()
|
||||
|
||||
|
||||
class BusMethodTests(unittest.TestCase):
|
||||
@pytest.fixture
|
||||
def log_tracker(bus):
|
||||
"""Return an instance of bus log tracker."""
|
||||
class LogTracker: # pylint: disable=too-few-public-methods
|
||||
"""Bus log tracker."""
|
||||
|
||||
def log(self, bus):
|
||||
self._log_entries = []
|
||||
log_entries = []
|
||||
|
||||
def logit(msg, level):
|
||||
self._log_entries.append(msg)
|
||||
bus.subscribe('log', logit)
|
||||
def __init__(self, bus):
|
||||
def logit(msg, level): # pylint: disable=unused-argument
|
||||
self.log_entries.append(msg)
|
||||
bus.subscribe('log', logit)
|
||||
|
||||
def assertLog(self, entries):
|
||||
self.assertEqual(self._log_entries, entries)
|
||||
return LogTracker(bus)
|
||||
|
||||
def get_listener(self, channel, index):
|
||||
def listener(arg=None):
|
||||
self.responses.append(msg % (index, channel, arg))
|
||||
return listener
|
||||
|
||||
def test_start(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
@pytest.fixture
|
||||
def listener():
|
||||
"""Return an instance of bus response tracker."""
|
||||
class Listner: # pylint: disable=too-few-public-methods
|
||||
"""Bus handler return value tracker."""
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('start', self.get_listener('start', index))
|
||||
responses = []
|
||||
|
||||
b.start()
|
||||
try:
|
||||
# The start method MUST call all 'start' listeners.
|
||||
self.assertEqual(
|
||||
set(self.responses),
|
||||
set([msg % (i, 'start', None) for i in range(num)]))
|
||||
# The start method MUST move the state to STARTED
|
||||
# (or EXITING, if errors occur)
|
||||
self.assertEqual(b.state, b.states.STARTED)
|
||||
# The start method MUST log its states.
|
||||
self.assertLog(['Bus STARTING', 'Bus STARTED'])
|
||||
finally:
|
||||
# Exit so the atexit handler doesn't complain.
|
||||
b.exit()
|
||||
def get_listener(self, channel, index):
|
||||
"""Return an argument tracking listener."""
|
||||
def listener(arg=None):
|
||||
self.responses.append(msg % (index, channel, arg))
|
||||
return listener
|
||||
|
||||
def test_stop(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
return Listner()
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('stop', self.get_listener('stop', index))
|
||||
|
||||
b.stop()
|
||||
def test_builtin_channels(bus, listener):
|
||||
"""Test that built-in channels trigger corresponding listeners."""
|
||||
expected = []
|
||||
|
||||
# The stop method MUST call all 'stop' listeners.
|
||||
self.assertEqual(set(self.responses),
|
||||
set([msg % (i, 'stop', None) for i in range(num)]))
|
||||
# The stop method MUST move the state to STOPPED
|
||||
self.assertEqual(b.state, b.states.STOPPED)
|
||||
# The stop method MUST log its states.
|
||||
self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
|
||||
for channel in bus.listeners:
|
||||
for index, priority in enumerate([100, 50, 0, 51]):
|
||||
bus.subscribe(
|
||||
channel,
|
||||
listener.get_listener(channel, index),
|
||||
priority,
|
||||
)
|
||||
|
||||
def test_graceful(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
for channel in bus.listeners:
|
||||
bus.publish(channel)
|
||||
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
|
||||
bus.publish(channel, arg=79347)
|
||||
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('graceful', self.get_listener('graceful', index))
|
||||
assert listener.responses == expected
|
||||
|
||||
b.graceful()
|
||||
|
||||
# The graceful method MUST call all 'graceful' listeners.
|
||||
self.assertEqual(
|
||||
set(self.responses),
|
||||
set([msg % (i, 'graceful', None) for i in range(num)]))
|
||||
# The graceful method MUST log its states.
|
||||
self.assertLog(['Bus graceful'])
|
||||
def test_custom_channels(bus, listener):
|
||||
"""Test that custom pub-sub channels work as built-in ones."""
|
||||
expected = []
|
||||
|
||||
def test_exit(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
custom_listeners = ('hugh', 'louis', 'dewey')
|
||||
for channel in custom_listeners:
|
||||
for index, priority in enumerate([None, 10, 60, 40]):
|
||||
bus.subscribe(
|
||||
channel,
|
||||
listener.get_listener(channel, index),
|
||||
priority,
|
||||
)
|
||||
|
||||
self.responses = []
|
||||
num = 3
|
||||
for index in range(num):
|
||||
b.subscribe('stop', self.get_listener('stop', index))
|
||||
b.subscribe('exit', self.get_listener('exit', index))
|
||||
for channel in custom_listeners:
|
||||
bus.publish(channel, 'ah so')
|
||||
expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2))
|
||||
bus.publish(channel)
|
||||
expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2))
|
||||
|
||||
b.exit()
|
||||
assert listener.responses == expected
|
||||
|
||||
# The exit method MUST call all 'stop' listeners,
|
||||
# and then all 'exit' listeners.
|
||||
self.assertEqual(set(self.responses),
|
||||
set([msg % (i, 'stop', None) for i in range(num)] +
|
||||
[msg % (i, 'exit', None) for i in range(num)]))
|
||||
# The exit method MUST move the state to EXITING
|
||||
self.assertEqual(b.state, b.states.EXITING)
|
||||
# The exit method MUST log its states.
|
||||
self.assertLog(
|
||||
|
||||
def test_listener_errors(bus, listener):
|
||||
"""Test that unhandled exceptions raise channel failures."""
|
||||
expected = []
|
||||
channels = [c for c in bus.listeners if c != 'log']
|
||||
|
||||
for channel in channels:
|
||||
bus.subscribe(channel, listener.get_listener(channel, 1))
|
||||
# This will break since the lambda takes no args.
|
||||
bus.subscribe(channel, lambda: None, priority=20)
|
||||
|
||||
for channel in channels:
|
||||
with pytest.raises(wspbus.ChannelFailures):
|
||||
bus.publish(channel, 123)
|
||||
expected.append(msg % (1, channel, 123))
|
||||
|
||||
assert listener.responses == expected
|
||||
|
||||
|
||||
def test_start(bus, listener, log_tracker):
|
||||
"""Test that bus start sequence calls all listeners."""
|
||||
num = 3
|
||||
for index in range(num):
|
||||
bus.subscribe('start', listener.get_listener('start', index))
|
||||
|
||||
bus.start()
|
||||
try:
|
||||
# The start method MUST call all 'start' listeners.
|
||||
assert (
|
||||
set(listener.responses) ==
|
||||
set(msg % (i, 'start', None) for i in range(num)))
|
||||
# The start method MUST move the state to STARTED
|
||||
# (or EXITING, if errors occur)
|
||||
assert bus.state == bus.states.STARTED
|
||||
# The start method MUST log its states.
|
||||
assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED']
|
||||
finally:
|
||||
# Exit so the atexit handler doesn't complain.
|
||||
bus.exit()
|
||||
|
||||
|
||||
def test_stop(bus, listener, log_tracker):
|
||||
"""Test that bus stop sequence calls all listeners."""
|
||||
num = 3
|
||||
|
||||
for index in range(num):
|
||||
bus.subscribe('stop', listener.get_listener('stop', index))
|
||||
|
||||
bus.stop()
|
||||
|
||||
# The stop method MUST call all 'stop' listeners.
|
||||
assert (set(listener.responses) ==
|
||||
set(msg % (i, 'stop', None) for i in range(num)))
|
||||
|
||||
# The stop method MUST move the state to STOPPED
|
||||
assert bus.state == bus.states.STOPPED
|
||||
|
||||
# The stop method MUST log its states.
|
||||
assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED']
|
||||
|
||||
|
||||
def test_graceful(bus, listener, log_tracker):
|
||||
"""Test that bus graceful state triggers all listeners."""
|
||||
num = 3
|
||||
|
||||
for index in range(num):
|
||||
bus.subscribe('graceful', listener.get_listener('graceful', index))
|
||||
|
||||
bus.graceful()
|
||||
|
||||
# The graceful method MUST call all 'graceful' listeners.
|
||||
assert (
|
||||
set(listener.responses) ==
|
||||
set(msg % (i, 'graceful', None) for i in range(num)))
|
||||
|
||||
# The graceful method MUST log its states.
|
||||
assert log_tracker.log_entries == ['Bus graceful']
|
||||
|
||||
|
||||
def test_exit(bus, listener, log_tracker):
|
||||
"""Test that bus exit sequence is correct."""
|
||||
num = 3
|
||||
|
||||
for index in range(num):
|
||||
bus.subscribe('stop', listener.get_listener('stop', index))
|
||||
bus.subscribe('exit', listener.get_listener('exit', index))
|
||||
|
||||
bus.exit()
|
||||
|
||||
# The exit method MUST call all 'stop' listeners,
|
||||
# and then all 'exit' listeners.
|
||||
assert (set(listener.responses) ==
|
||||
set([msg % (i, 'stop', None) for i in range(num)] +
|
||||
[msg % (i, 'exit', None) for i in range(num)]))
|
||||
|
||||
# The exit method MUST move the state to EXITING
|
||||
assert bus.state == bus.states.EXITING
|
||||
|
||||
# The exit method MUST log its states.
|
||||
assert (log_tracker.log_entries ==
|
||||
['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
|
||||
|
||||
def test_wait(self):
|
||||
b = wspbus.Bus()
|
||||
|
||||
def f(method):
|
||||
time.sleep(0.2)
|
||||
getattr(b, method)()
|
||||
def test_wait(bus):
|
||||
"""Test that bus wait awaits for states."""
|
||||
def f(method): # pylint: disable=invalid-name
|
||||
time.sleep(0.2)
|
||||
getattr(bus, method)()
|
||||
|
||||
for method, states in [('start', [b.states.STARTED]),
|
||||
('stop', [b.states.STOPPED]),
|
||||
('start',
|
||||
[b.states.STARTING, b.states.STARTED]),
|
||||
('exit', [b.states.EXITING]),
|
||||
]:
|
||||
threading.Thread(target=f, args=(method,)).start()
|
||||
b.wait(states)
|
||||
flow = [
|
||||
('start', [bus.states.STARTED]),
|
||||
('stop', [bus.states.STOPPED]),
|
||||
('start', [bus.states.STARTING, bus.states.STARTED]),
|
||||
('exit', [bus.states.EXITING]),
|
||||
]
|
||||
|
||||
# The wait method MUST wait for the given state(s).
|
||||
if b.state not in states:
|
||||
self.fail('State %r not in %r' % (b.state, states))
|
||||
for method, states in flow:
|
||||
threading.Thread(target=f, args=(method,)).start()
|
||||
bus.wait(states)
|
||||
|
||||
def test_block(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
|
||||
def f():
|
||||
time.sleep(0.2)
|
||||
b.exit()
|
||||
|
||||
def g():
|
||||
time.sleep(0.4)
|
||||
threading.Thread(target=f).start()
|
||||
threading.Thread(target=g).start()
|
||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||
self.assertEqual(len(threads), 3)
|
||||
|
||||
b.block()
|
||||
|
||||
# The block method MUST wait for the EXITING state.
|
||||
self.assertEqual(b.state, b.states.EXITING)
|
||||
# The block method MUST wait for ALL non-main, non-daemon threads to
|
||||
# finish.
|
||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||
self.assertEqual(len(threads), 1)
|
||||
# The last message will mention an indeterminable thread name; ignore
|
||||
# it
|
||||
self.assertEqual(self._log_entries[:-1],
|
||||
['Bus STOPPING', 'Bus STOPPED',
|
||||
'Bus EXITING', 'Bus EXITED',
|
||||
'Waiting for child threads to terminate...'])
|
||||
|
||||
def test_start_with_callback(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
try:
|
||||
events = []
|
||||
|
||||
def f(*args, **kwargs):
|
||||
events.append(('f', args, kwargs))
|
||||
|
||||
def g():
|
||||
events.append('g')
|
||||
b.subscribe('start', g)
|
||||
b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
|
||||
# Give wait() time to run f()
|
||||
time.sleep(0.2)
|
||||
|
||||
# The callback method MUST wait for the STARTED state.
|
||||
self.assertEqual(b.state, b.states.STARTED)
|
||||
# The callback method MUST run after all start methods.
|
||||
self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
|
||||
finally:
|
||||
b.exit()
|
||||
|
||||
def test_log(self):
|
||||
b = wspbus.Bus()
|
||||
self.log(b)
|
||||
self.assertLog([])
|
||||
|
||||
# Try a normal message.
|
||||
expected = []
|
||||
for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
|
||||
b.log(msg)
|
||||
expected.append(msg)
|
||||
self.assertLog(expected)
|
||||
|
||||
# Try an error message
|
||||
try:
|
||||
foo
|
||||
except NameError:
|
||||
b.log('You are lost and gone forever', traceback=True)
|
||||
lastmsg = self._log_entries[-1]
|
||||
if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
|
||||
self.fail('Last log message %r did not contain '
|
||||
'the expected traceback.' % lastmsg)
|
||||
else:
|
||||
self.fail('NameError was not raised as expected.')
|
||||
# The wait method MUST wait for the given state(s).
|
||||
assert bus.state in states, 'State %r not in %r' % (bus.state, states)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@pytest.mark.xfail(CI_ON_MACOS, reason='continuous integration on macOS fails')
|
||||
def test_wait_publishes_periodically(bus):
|
||||
"""Test that wait publishes each tick."""
|
||||
callback = unittest.mock.MagicMock()
|
||||
bus.subscribe('main', callback)
|
||||
|
||||
def set_start():
|
||||
time.sleep(0.05)
|
||||
bus.start()
|
||||
threading.Thread(target=set_start).start()
|
||||
bus.wait(bus.states.STARTED, interval=0.01, channel='main')
|
||||
assert callback.call_count > 3
|
||||
|
||||
|
||||
def test_block(bus, log_tracker):
|
||||
"""Test that bus block waits for exiting."""
|
||||
def f(): # pylint: disable=invalid-name
|
||||
time.sleep(0.2)
|
||||
bus.exit()
|
||||
|
||||
def g(): # pylint: disable=invalid-name
|
||||
time.sleep(0.4)
|
||||
|
||||
threading.Thread(target=f).start()
|
||||
threading.Thread(target=g).start()
|
||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||
assert len(threads) == 3
|
||||
|
||||
bus.block()
|
||||
|
||||
# The block method MUST wait for the EXITING state.
|
||||
assert bus.state == bus.states.EXITING
|
||||
|
||||
# The block method MUST wait for ALL non-main, non-daemon threads to
|
||||
# finish.
|
||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||
assert len(threads) == 1
|
||||
|
||||
# The last message will mention an indeterminable thread name; ignore
|
||||
# it
|
||||
expected_bus_messages = [
|
||||
'Bus STOPPING',
|
||||
'Bus STOPPED',
|
||||
'Bus EXITING',
|
||||
'Bus EXITED',
|
||||
'Waiting for child threads to terminate...',
|
||||
]
|
||||
bus_msg_num = len(expected_bus_messages)
|
||||
|
||||
# If the last message mentions an indeterminable thread name then ignore it
|
||||
assert log_tracker.log_entries[:bus_msg_num] == expected_bus_messages
|
||||
assert len(log_tracker.log_entries[bus_msg_num:]) <= 1, (
|
||||
'No more than one extra log line with the thread name expected'
|
||||
)
|
||||
|
||||
|
||||
def test_start_with_callback(bus):
|
||||
"""Test that callback fires on bus start."""
|
||||
try:
|
||||
events = []
|
||||
|
||||
def f(*args, **kwargs): # pylint: disable=invalid-name
|
||||
events.append(('f', args, kwargs))
|
||||
|
||||
def g(): # pylint: disable=invalid-name
|
||||
events.append('g')
|
||||
bus.subscribe('start', g)
|
||||
bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
|
||||
|
||||
# Give wait() time to run f()
|
||||
time.sleep(0.2)
|
||||
|
||||
# The callback method MUST wait for the STARTED state.
|
||||
assert bus.state == bus.states.STARTED
|
||||
|
||||
# The callback method MUST run after all start methods.
|
||||
assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})]
|
||||
finally:
|
||||
bus.exit()
|
||||
|
||||
|
||||
def test_log(bus, log_tracker):
|
||||
"""Test that bus messages and errors are logged."""
|
||||
assert log_tracker.log_entries == []
|
||||
|
||||
# Try a normal message.
|
||||
expected = []
|
||||
for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
|
||||
bus.log(msg_)
|
||||
expected.append(msg_)
|
||||
assert log_tracker.log_entries == expected
|
||||
|
||||
# Try an error message
|
||||
try:
|
||||
foo
|
||||
except NameError:
|
||||
bus.log('You are lost and gone forever', traceback=True)
|
||||
lastmsg = log_tracker.log_entries[-1]
|
||||
assert 'Traceback' in lastmsg and 'NameError' in lastmsg, (
|
||||
'Last log message %r did not contain '
|
||||
'the expected traceback.' % lastmsg
|
||||
)
|
||||
else:
|
||||
pytest.fail('NameError was not raised as expected.')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue