Watchdog unit-tests, bugfixes and questions (#449)

Implement missing unit-tests for and drop unused code
This commit is contained in:
Alexander Kukushkin
2017-07-11 10:00:30 +02:00
committed by GitHub
parent 0b2134aba3
commit acc6d7c2c2
10 changed files with 198 additions and 86 deletions
+4 -2
View File
@@ -81,6 +81,7 @@ class AbstractController(object):
def cancel_background(self): def cancel_background(self):
pass pass
class PatroniController(AbstractController): class PatroniController(AbstractController):
__PORT = 5440 __PORT = 5440
PATRONI_CONFIG = '{}.yml' PATRONI_CONFIG = '{}.yml'
@@ -275,6 +276,7 @@ class PatroniController(AbstractController):
if 'process' not in p.cmdline()[0]: if 'process' not in p.cmdline()[0]:
p.terminate() p.terminate()
class ProcessHang(object): class ProcessHang(object):
"""A background thread implementing a cancelable process hang via SIGSTOP.""" """A background thread implementing a cancelable process hang via SIGSTOP."""
@@ -499,7 +501,8 @@ class PatroniPoolController(object):
def start(self, name, max_wait_limit=20, tags=None, with_watchdog=False): def start(self, name, max_wait_limit=20, tags=None, with_watchdog=False):
if name not in self._processes: if name not in self._processes:
self._processes[name] = PatroniController(self._context, name, self.patroni_path, self._output_dir, tags, with_watchdog=with_watchdog) self._processes[name] = PatroniController(self._context, name, self.patroni_path,
self._output_dir, tags, with_watchdog)
self._processes[name].start(max_wait_limit) self._processes[name].start(max_wait_limit)
def __getattr__(self, func): def __getattr__(self, func):
@@ -637,7 +640,6 @@ class WatchdogMonitor(object):
self._thread.join() self._thread.join()
self._thread = None self._thread = None
def reset(self): def reset(self):
self._log("reset") self._log("reset")
self.was_pinged = self.was_closed = self._was_triggered = False self.was_pinged = self.was_closed = self._was_triggered = False
+2 -1
View File
@@ -111,7 +111,8 @@ def check_response(context, component, data):
assert context.status_code == int(data),\ assert context.status_code == int(data),\
"status code {0} != {1}, response: {2}".format(context.status_code, data, context.response) "status code {0} != {1}, response: {2}".format(context.status_code, data, context.response)
elif component == 'returncode': elif component == 'returncode':
assert context.status_code == int(data), "return code {0} != {1}, {2}".format(context.status_code, data, context.response) assert context.status_code == int(data), "return code {0} != {1}, {2}".format(context.status_code,
data, context.response)
elif component == 'text': elif component == 'text':
assert context.response == data.strip('"'), "response {0} does not contain {1}".format(context.response, data) assert context.response == data.strip('"'), "response {0} does not contain {1}".format(context.response, data)
elif component == 'output': elif component == 'output':
+1 -1
View File
@@ -1,6 +1,7 @@
from behave import step, then from behave import step, then
import time import time
def polling_loop(timeout, interval=1): def polling_loop(timeout, interval=1):
"""Returns an iterator that returns values until timeout has passed. Timeout is measured from start of iteration.""" """Returns an iterator that returns values until timeout has passed. Timeout is measured from start of iteration."""
start_time = time.time() start_time = time.time()
@@ -12,7 +13,6 @@ def polling_loop(timeout, interval=1):
time.sleep(interval) time.sleep(interval)
@step('I start {name:w} with watchdog') @step('I start {name:w} with watchdog')
def start_patroni_with_watchdog(context, name): def start_patroni_with_watchdog(context, name):
return context.pctl.start(name, with_watchdog=True) return context.pctl.start(name, with_watchdog=True)
+1 -49
View File
@@ -954,13 +954,9 @@ class Postgresql(object):
def _wait_for_connection_close(self, pid): def _wait_for_connection_close(self, pid):
try: try:
with self.connection().cursor() as cur: with self.connection().cursor() as cur:
while True: # Need a timeout here? while pid == self.get_pid() and self.is_pid_running(pid): # Need a timeout here?
if pid == self.get_pid() and self.is_pid_running(pid):
cur.execute("SELECT 1") cur.execute("SELECT 1")
time.sleep(STOP_POLLING_INTERVAL) time.sleep(STOP_POLLING_INTERVAL)
continue
else:
break
except psycopg2.Error: except psycopg2.Error:
pass pass
@@ -1319,50 +1315,6 @@ class Postgresql(object):
self.call_nowait(ACTION_ON_ROLE_CHANGE) self.call_nowait(ACTION_ON_ROLE_CHANGE)
return True return True
def _do_rewind(self, leader):
logger.info("rewind flag is set")
if self.is_running() and not self.stop(checkpoint=False):
logger.warning('Can not run pg_rewind because postgres is still running')
return False
# prepare pg_rewind connection
r = leader.conn_kwargs(self._superuser)
# first make sure that we are really trying to rewind
# from the master and run a checkpoint on a t in order to
# make it store the new timeline ([email protected])
leader_status = self.checkpoint(r)
if leader_status:
logger.warning('Can not use %s for rewind: %s', leader.name, leader_status)
return False
# at present, pg_rewind only runs when the cluster is shut down cleanly
# and not shutdown in recovery. We have to remove the recovery.conf if present
# and start/shutdown in a single user mode to emulate this.
# XXX: if recovery.conf is linked, it will be written anew as a normal file.
if os.path.isfile(self._recovery_conf) or os.path.islink(self._recovery_conf):
os.unlink(self._recovery_conf)
# Archived segments might be useful to pg_rewind,
# clean the flags that tell we should remove them.
self.cleanup_archive_status()
# Start in a single user mode and stop to produce a clean shutdown
opts = self.read_postmaster_opts()
opts.update({'archive_mode': 'on', 'archive_command': 'false'})
self.single_user_mode(options=opts)
try:
if not self.rewind(r):
logger.error('unable to rewind the former master')
if self.config.get('remove_data_directory_on_rewind_failure', False):
self.remove_data_directory()
return False
return True
finally:
self._need_rewind = False
def save_configuration_files(self): def save_configuration_files(self):
""" """
copy postgresql.conf to postgresql.conf.backup to be able to retrive configuration files copy postgresql.conf to postgresql.conf.backup to be able to retrive configuration files
+2 -2
View File
@@ -94,7 +94,7 @@ class Watchdog(object):
logger.info("{0} activated with {1} second timeout, timing slack {2} seconds" logger.info("{0} activated with {1} second timeout, timing slack {2} seconds"
.format(self.impl.describe(), actual_timeout, slack)) .format(self.impl.describe(), actual_timeout, slack))
else: else:
if self.mode == MODE_REQUIRED: if self.mode == MODE_REQUIRED: # XXX: can we really get here?
logger.error("Configuration requires watchdog, but watchdog could not be activated") logger.error("Configuration requires watchdog, but watchdog could not be activated")
sys.exit(1) sys.exit(1)
@@ -116,7 +116,7 @@ class Watchdog(object):
logger.error("Error while sending keepalive: %s", e) logger.error("Error while sending keepalive: %s", e)
def _get_impl(self): def _get_impl(self):
if self.mode not in [MODE_AUTOMATIC, MODE_REQUIRED]: if self.mode not in [MODE_AUTOMATIC, MODE_REQUIRED]: # XXX: can't be reached
return NullWatchdog() return NullWatchdog()
if self.driver == 'testing': if self.driver == 'testing':
+2 -2
View File
@@ -145,7 +145,7 @@ class LinuxWatchdogDevice(WatchdogBase):
os.close(self._fd) os.close(self._fd)
self._fd = None self._fd = None
except OSError as e: except OSError as e:
return WatchdogError("Error while closing {0}: {1}".format(self.describe(), e)) raise WatchdogError("Error while closing {0}: {1}".format(self.describe(), e))
@property @property
def can_be_disabled(self): def can_be_disabled(self):
@@ -176,7 +176,7 @@ class LinuxWatchdogDevice(WatchdogBase):
try: try:
_, version, identity = self.get_support() _, version, identity = self.get_support()
ver_str = " (firmware {0})".format(version) if version else "" ver_str = " (firmware {0})".format(version) if version else ""
except WatchdogError: except WatchdogError: # XXX: Can it really be raise when self._fd is not None?
pass pass
return identity + ver_str + dev_str return identity + ver_str + dev_str
+9 -1
View File
@@ -1,7 +1,7 @@
import unittest import unittest
from mock import Mock, patch from mock import Mock, patch
from patroni.async_executor import AsyncExecutor from patroni.async_executor import AsyncExecutor, CriticalTask
from threading import Thread from threading import Thread
@@ -16,3 +16,11 @@ class TestAsyncExecutor(unittest.TestCase):
def test_run(self): def test_run(self):
self.a.run(Mock(side_effect=Exception())) self.a.run(Mock(side_effect=Exception()))
class TestCriticalTask(unittest.TestCase):
def test_completed_task(self):
ct = CriticalTask()
ct.complete(1)
self.assertFalse(ct.cancel())
+23 -1
View File
@@ -1,6 +1,7 @@
import datetime import datetime
import etcd import etcd
import os import os
import time
import unittest import unittest
from mock import Mock, MagicMock, PropertyMock, patch from mock import Mock, MagicMock, PropertyMock, patch
@@ -8,12 +9,13 @@ from patroni.config import Config
from patroni.dcs import Cluster, ClusterConfig, Failover, Leader, Member, get_dcs, SyncState from patroni.dcs import Cluster, ClusterConfig, Failover, Leader, Member, get_dcs, SyncState
from patroni.dcs.etcd import Client from patroni.dcs.etcd import Client
from patroni.exceptions import DCSError, PostgresException from patroni.exceptions import DCSError, PostgresException
from patroni.ha import Ha, _MemberStatus from patroni.ha import Ha, _MemberStatus, BackgroundKeepaliveSender
from patroni.postgresql import Postgresql from patroni.postgresql import Postgresql
from patroni.watchdog import Watchdog from patroni.watchdog import Watchdog
from patroni.utils import tzutc from patroni.utils import tzutc
from test_etcd import socket_getaddrinfo, etcd_read, etcd_write, requests_get from test_etcd import socket_getaddrinfo, etcd_read, etcd_write, requests_get
from test_postgresql import psycopg2_connect from test_postgresql import psycopg2_connect
from threading import Event
def true(*args, **kwargs): def true(*args, **kwargs):
@@ -246,6 +248,7 @@ class TestHa(unittest.TestCase):
def test_demote_because_not_having_lock(self): def test_demote_because_not_having_lock(self):
self.ha.cluster.is_unlocked = false self.ha.cluster.is_unlocked = false
with patch.object(Watchdog, 'is_running', PropertyMock(return_value=True)):
self.assertEquals(self.ha.run_cycle(), 'demoting self because i do not have the lock and i was a leader') self.assertEquals(self.ha.run_cycle(), 'demoting self because i do not have the lock and i was a leader')
def test_demote_because_update_lock_failed(self): def test_demote_because_update_lock_failed(self):
@@ -335,6 +338,7 @@ class TestHa(unittest.TestCase):
with patch.object(self.ha, "restart_matches", return_value=False): with patch.object(self.ha, "restart_matches", return_value=False):
self.assertEquals(self.ha.restart({'foo': 'bar'}), (False, "restart conditions are not satisfied")) self.assertEquals(self.ha.restart({'foo': 'bar'}), (False, "restart conditions are not satisfied"))
@patch('os.kill', Mock())
def test_restart_in_progress(self): def test_restart_in_progress(self):
with patch('patroni.async_executor.AsyncExecutor.busy', PropertyMock(return_value=True)): with patch('patroni.async_executor.AsyncExecutor.busy', PropertyMock(return_value=True)):
self.ha.restart({}, run_async=True) self.ha.restart({}, run_async=True)
@@ -349,6 +353,7 @@ class TestHa(unittest.TestCase):
self.ha.update_lock = false self.ha.update_lock = false
self.p.set_role('master') self.p.set_role('master')
with patch('patroni.async_executor.CriticalTask.cancel', Mock(return_value=False)):
with patch('patroni.postgresql.Postgresql.stop') as stop_mock: with patch('patroni.postgresql.Postgresql.stop') as stop_mock:
self.assertEquals(self.ha.run_cycle(), 'lost leader lock during restart') self.assertEquals(self.ha.run_cycle(), 'lost leader lock during restart')
stop_mock.assert_called() stop_mock.assert_called()
@@ -797,6 +802,10 @@ class TestHa(unittest.TestCase):
def test_wakup(self): def test_wakup(self):
self.ha.wakeup() self.ha.wakeup()
def test_shutdown(self):
self.p.is_running = false
self.ha.shutdown()
@patch('time.sleep', Mock()) @patch('time.sleep', Mock())
def test_leader_with_empty_directory(self): def test_leader_with_empty_directory(self):
self.ha.cluster = get_cluster_initialized_with_leader() self.ha.cluster = get_cluster_initialized_with_leader()
@@ -808,3 +817,16 @@ class TestHa(unittest.TestCase):
self.ha.has_lock = false self.ha.has_lock = false
# will not say bootstrap from leader as replica can't self elect # will not say bootstrap from leader as replica can't self elect
self.assertEquals(self.ha.run_cycle(), "trying to bootstrap from replica 'other'") self.assertEquals(self.ha.run_cycle(), "trying to bootstrap from replica 'other'")
class TestBackgroundKeepaliveSender(unittest.TestCase):
def test_run(self):
safe_event = Event()
ha = Mock()
ha.dcs.loop_wait = 0.1
with BackgroundKeepaliveSender(ha, safe_event):
time.sleep(1)
safe_event.set()
time.sleep(1)
self.assertTrue(ha.keepalive.call_count > 2)
+59 -5
View File
@@ -1,3 +1,4 @@
import errno
import mock # for the mock.call method, importing it without a namespace breaks python3 import mock # for the mock.call method, importing it without a namespace breaks python3
import os import os
import psycopg2 import psycopg2
@@ -6,6 +7,7 @@ import subprocess
import unittest import unittest
from mock import Mock, MagicMock, PropertyMock, patch, mock_open from mock import Mock, MagicMock, PropertyMock, patch, mock_open
from patroni.async_executor import CriticalTask
from patroni.dcs import Cluster, Leader, Member, SyncState from patroni.dcs import Cluster, Leader, Member, SyncState
from patroni.exceptions import PostgresException, PostgresConnectionException from patroni.exceptions import PostgresException, PostgresConnectionException
from patroni.postgresql import Postgresql, STATE_REJECT, STATE_NO_RESPONSE from patroni.postgresql import Postgresql, STATE_REJECT, STATE_NO_RESPONSE
@@ -216,13 +218,13 @@ class TestPostgresql(unittest.TestCase):
mock_is_running.return_value = True mock_is_running.return_value = True
mock_wait_for_port_open.return_value = True mock_wait_for_port_open.return_value = True
mock_wait_for_startup.return_value = False mock_wait_for_startup.return_value = False
mock_popen.stdout.readline.return_value = '123' mock_popen.return_value.stdout.readline.return_value = '123'
self.assertTrue(self.p.start()) self.assertTrue(self.p.start())
mock_is_running.return_value = False mock_is_running.return_value = False
open(os.path.join(self.data_dir, 'postmaster.pid'), 'w').close() open(os.path.join(self.data_dir, 'postmaster.pid'), 'w').close()
pg_conf = os.path.join(self.data_dir, 'postgresql.conf') pg_conf = os.path.join(self.data_dir, 'postgresql.conf')
open(pg_conf, 'w').close() open(pg_conf, 'w').close()
self.assertFalse(self.p.start()) self.assertFalse(self.p.start(task=CriticalTask()))
with open(pg_conf) as f: with open(pg_conf) as f:
lines = f.readlines() lines = f.readlines()
self.assertTrue("f.oo = 'bar'\n" in lines) self.assertTrue("f.oo = 'bar'\n" in lines)
@@ -233,6 +235,9 @@ class TestPostgresql(unittest.TestCase):
mock_wait_for_port_open.return_value = False mock_wait_for_port_open.return_value = False
self.assertFalse(self.p.start()) self.assertFalse(self.p.start())
task = CriticalTask()
task.cancel()
self.assertFalse(self.p.start(task=task))
@patch.object(Postgresql, 'pg_isready') @patch.object(Postgresql, 'pg_isready')
@patch.object(Postgresql, 'read_pid_file') @patch.object(Postgresql, 'read_pid_file')
@@ -266,12 +271,23 @@ class TestPostgresql(unittest.TestCase):
mock_pg_isready.return_value = 'garbage' mock_pg_isready.return_value = 'garbage'
self.assertTrue(self.p.wait_for_port_open(42, 100., 1)) self.assertTrue(self.p.wait_for_port_open(42, 100., 1))
@patch('time.sleep', Mock())
@patch.object(Postgresql, 'is_running') @patch.object(Postgresql, 'is_running')
def test_stop(self, mock_is_running): @patch.object(Postgresql, 'get_pid')
def test_stop(self, mock_get_pid, mock_is_running):
mock_is_running.return_value = True mock_is_running.return_value = True
mock_get_pid.return_value = 0
self.assertTrue(self.p.stop()) self.assertTrue(self.p.stop())
with patch('subprocess.call', Mock(return_value=1)): mock_get_pid.return_value = -1
mock_is_running.return_value = False self.assertFalse(self.p.stop())
mock_get_pid.return_value = 123
with patch('os.kill', Mock(side_effect=[OSError(errno.ESRCH, ''), OSError, None])):
self.assertTrue(self.p.stop())
self.assertFalse(self.p.stop())
self.p.stop_safepoint_reached.clear()
self.assertTrue(self.p.stop())
with patch.object(Postgresql, '_signal_postmaster_stop', Mock(return_value=(123, None))):
with patch.object(Postgresql, 'is_pid_running', Mock(side_effect=[True, False, False])):
self.assertTrue(self.p.stop()) self.assertTrue(self.p.stop())
def test_restart(self): def test_restart(self):
@@ -778,3 +794,41 @@ class TestPostgresql(unittest.TestCase):
self.p.get_server_parameters(config) self.p.get_server_parameters(config)
self.p.set_synchronous_standby('foo') self.p.set_synchronous_standby('foo')
self.p.get_server_parameters(config) self.p.get_server_parameters(config)
@patch.object(Postgresql, 'read_pid_file', Mock(return_value={'pid': 'z'}))
def test_get_pid(self):
self.p.get_pid()
@patch.object(Postgresql, 'is_running', Mock(return_value=True))
@patch.object(Postgresql, '_signal_postmaster_stop', Mock(return_value=(123, None)))
@patch.object(Postgresql, 'get_pid', Mock(return_value=123))
@patch('time.sleep', Mock())
@patch.object(Postgresql, 'is_pid_running')
def test__wait_for_connection_close(self, mock_is_pid_running):
mock_is_pid_running.side_effect = [True, False, False]
self.p.stop_safepoint_reached.clear()
self.p.stop()
mock_is_pid_running.side_effect = [True, False, False]
self.p.stop_safepoint_reached.clear()
with patch.object(MockCursor, "execute", Mock(side_effect=psycopg2.Error)):
self.p.stop()
@patch.object(Postgresql, 'is_running', Mock(return_value=True))
@patch.object(Postgresql, '_signal_postmaster_stop', Mock(return_value=(123, None)))
@patch.object(Postgresql, 'get_pid', Mock(return_value=123))
@patch.object(Postgresql, 'is_pid_running', Mock(return_value=False))
@patch('psutil.Process')
def test__wait_for_user_backends_to_close(self, mock_psutil):
child = Mock()
child.cmdline.return_value = ['foo']
mock_psutil.return_value.children.return_value = [child]
self.p.stop_safepoint_reached.clear()
self.p.stop()
@patch('os.kill', Mock(side_effect=[OSError(errno.ESRCH, ''), OSError]))
@patch('time.sleep', Mock())
@patch.object(Postgresql, 'is_pid_running', Mock(side_effect=[True, False]))
def test_terminate_starting_postmaster(self):
self.p.terminate_starting_postmaster(123)
self.p.terminate_starting_postmaster(123)
+88 -15
View File
@@ -1,12 +1,13 @@
import unittest
from mock import patch
import platform
import ctypes import ctypes
from patroni.watchdog import Watchdog
import patroni.watchdog.linux as linuxwd import patroni.watchdog.linux as linuxwd
import sys import sys
import unittest
from mock import patch, Mock, PropertyMock
from patroni.watchdog import Watchdog, WatchdogError
from patroni.watchdog.base import NullWatchdog
from patroni.watchdog.linux import LinuxWatchdogDevice
class MockDevice(object): class MockDevice(object):
def __init__(self, fd, filename, flag): def __init__(self, fd, filename, flag):
@@ -20,41 +21,46 @@ class MockDevice(object):
mock_devices = [None] mock_devices = [None]
def mock_open(filename, flag): def mock_open(filename, flag):
fd = len(mock_devices) fd = len(mock_devices)
mock_devices.append(MockDevice(fd, filename, flag)) mock_devices.append(MockDevice(fd, filename, flag))
return fd return fd
def mock_ioctl(fd, op, arg=None, mutate_flag=False): def mock_ioctl(fd, op, arg=None, mutate_flag=False):
assert 0 < fd < len(mock_devices) assert 0 < fd < len(mock_devices)
dev = mock_devices[fd] dev = mock_devices[fd]
sys.stderr.write("Ioctl %d %d %r\n" %( fd, op, arg)) sys.stderr.write("Ioctl %d %d %r\n" % (fd, op, arg))
if op == linuxwd.WDIOC_GETSUPPORT: if op == linuxwd.WDIOC_GETSUPPORT:
sys.stderr.write("Get support\n") sys.stderr.write("Get support\n")
assert(mutate_flag == True) assert(mutate_flag is True)
arg.options = sum(map(linuxwd.WDIOF.get, ['SETTIMEOUT', 'KEEPALIVEPING', 'MAGICCLOSE'])) arg.options = sum(map(linuxwd.WDIOF.get, ['SETTIMEOUT', 'KEEPALIVEPING']))
arg.identity = (ctypes.c_ubyte*32)(*map(ord, 'Mock Watchdog')) arg.identity = (ctypes.c_ubyte*32)(*map(ord, 'Mock Watchdog'))
elif op == linuxwd.WDIOC_GETTIMEOUT: elif op == linuxwd.WDIOC_GETTIMEOUT:
arg.value = dev.timeout arg.value = dev.timeout
elif op == linuxwd.WDIOC_SETTIMEOUT: elif op == linuxwd.WDIOC_SETTIMEOUT:
sys.stderr.write("Set timeout called with %s\n" % arg.value) sys.stderr.write("Set timeout called with %s\n" % arg.value)
assert 0 < arg.value < 65535 assert 0 < arg.value < 65535
dev.timeout = arg.value dev.timeout = arg.value - 1
else: else:
raise Exception("Unknown op %d", op) raise Exception("Unknown op %d", op)
return 0 return 0
def mock_write(fd, string): def mock_write(fd, string):
assert 0 < fd < len(mock_devices) assert 0 < fd < len(mock_devices)
assert len(string) == 1 assert len(string) == 1
assert mock_devices[fd].open assert mock_devices[fd].open
mock_devices[fd].writes.append(string) mock_devices[fd].writes.append(string)
def mock_close(fd): def mock_close(fd):
assert 0 < fd < len(mock_devices) assert 0 < fd < len(mock_devices)
assert mock_devices[fd].open assert mock_devices[fd].open
mock_devices[fd].open = False mock_devices[fd].open = False
@patch('os.open', mock_open) @patch('os.open', mock_open)
@patch('os.write', mock_write) @patch('os.write', mock_write)
@patch('os.close', mock_close) @patch('os.close', mock_close)
@@ -63,18 +69,32 @@ class TestWatchdog(unittest.TestCase):
def setUp(self): def setUp(self):
mock_devices[:] = [None] mock_devices[:] = [None]
@patch('platform.system', Mock(return_value='Linux'))
@patch.object(LinuxWatchdogDevice, 'can_be_disabled', PropertyMock(return_value=True))
def test_unsafe_timeout_disable_watchdog_and_exit(self):
self.assertRaises(SystemExit, Watchdog({'ttl': 30, 'loop_wait': 15, 'watchdog': {'mode': 'required'}}).activate)
@patch('platform.system', Mock(return_value='Linux'))
@patch.object(LinuxWatchdogDevice, 'get_timeout', Mock(return_value=16))
def test_timeout_does_not_ensure_safe_termination(self):
Watchdog({'ttl': 30, 'loop_wait': 15, 'watchdog': {'mode': 'auto'}}).activate()
self.assertEquals(len(mock_devices), 2)
@patch('platform.system', Mock(return_value='Linux'))
@patch.object(Watchdog, 'is_running', PropertyMock(return_value=False))
def test_watchdog_not_activated(self):
self.assertRaises(SystemExit, Watchdog({'ttl': 30, 'loop_wait': 10, 'watchdog': {'mode': 'required'}}).activate)
@patch('platform.system', Mock(return_value='Linux'))
def test_basic_operation(self): def test_basic_operation(self):
if platform.system() != 'Linux':
return
watchdog = Watchdog({'ttl': 30, 'loop_wait': 10, 'watchdog': {'mode': 'required'}}) watchdog = Watchdog({'ttl': 30, 'loop_wait': 10, 'watchdog': {'mode': 'required'}})
watchdog.activate() watchdog.activate()
self.assertEquals(len(mock_devices), 2) self.assertEquals(len(mock_devices), 2)
device = mock_devices[-1] device = mock_devices[-1]
self.assertTrue(device.open) self.assertTrue(device.open)
self.assertEquals(device.timeout, 15) self.assertEquals(device.timeout, 14)
watchdog.keepalive() watchdog.keepalive()
self.assertEquals(len(device.writes), 1) self.assertEquals(len(device.writes), 1)
@@ -88,3 +108,56 @@ class TestWatchdog(unittest.TestCase):
watchdog.activate() watchdog.activate()
self.assertEquals(len(mock_devices), 1) self.assertEquals(len(mock_devices), 1)
self.assertFalse(watchdog.is_running) self.assertFalse(watchdog.is_running)
def test_parse_mode(self):
with patch('patroni.watchdog.base.logger.warning', new_callable=Mock()) as warning_mock:
watchdog = Watchdog({'ttl': 30, 'loop_wait': 10, 'watchdog': {'mode': 'bad'}})
self.assertEquals(watchdog.mode, 'off')
warning_mock.assert_called_once()
@patch('platform.system', Mock(return_value='Unknown'))
def test_unsupported_platform(self):
self.assertRaises(SystemExit, Watchdog, {'ttl': 30, 'loop_wait': 10, 'watchdog': {'mode': 'required'}})
def test_exceptions(self):
wd = Watchdog({'ttl': 30, 'loop_wait': 10, 'watchdog': {'mode': 'bad'}})
wd.impl.close = wd.impl.keepalive = Mock(side_effect=WatchdogError(''))
self.assertIsNone(wd.disable())
self.assertIsNone(wd.keepalive())
class TestNullWatchdog(unittest.TestCase):
def test_basics(self):
watchdog = NullWatchdog()
self.assertTrue(watchdog.can_be_disabled)
self.assertRaises(WatchdogError, watchdog.set_timeout, 1)
self.assertEquals(watchdog.describe(), 'NullWatchdog')
self.assertIsInstance(NullWatchdog.from_config({}), NullWatchdog)
class TestLinuxWatchdogDevice(unittest.TestCase):
def setUp(self):
self.impl = LinuxWatchdogDevice.from_config({})
@patch('os.open', Mock(return_value=3))
@patch('os.write', Mock(side_effect=OSError))
@patch('fcntl.ioctl', Mock(return_value=0))
def test_basics(self):
self.impl.open()
try:
if self.impl.get_support().has_foo:
self.assertFail()
except Exception as e:
self.assertTrue(isinstance(e, AttributeError))
self.assertRaises(WatchdogError, self.impl.close)
self.assertRaises(WatchdogError, self.impl.keepalive)
self.assertRaises(WatchdogError, self.impl.set_timeout, -1)
@patch('os.open', Mock(return_value=3))
@patch('fcntl.ioctl', Mock(return_value=-1))
def test__ioctl(self):
self.assertRaises(WatchdogError, self.impl.get_support)
self.impl.open()
self.assertRaises(IOError, self.impl.get_support)