474 lines
16 KiB
Python
474 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import unittest
|
|
from pgbouncemgr.logger import *
|
|
from pgbouncemgr.state import *
|
|
from pgbouncemgr.constants import \
|
|
LEADER_UNKNOWN, LEADER_CONNECTED, NODE_PRIMARY, NODE_STANDBY
|
|
|
|
|
|
PGBOUNCER_CONFIG = os.path.join(
|
|
os.path.dirname(os.path.realpath(__file__)),
|
|
"testfiles", "pgbouncer.ini")
|
|
|
|
|
|
class StateTests(unittest.TestCase):
|
|
def test_GivenFreshState_DefaultsAreSetCorrectly(self):
|
|
state = State(Logger())
|
|
self.assertIsNone(state.system_id)
|
|
self.assertIsNone(state.timeline_id)
|
|
self.assertIsNone(state.active_pgbouncer_config)
|
|
self.assertIsNone(state.leader_node_id)
|
|
self.assertIsNone(state.leader_error)
|
|
self.assertEqual(LEADER_UNKNOWN, state.leader_status)
|
|
self.assertEqual({}, state.nodes)
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_GivenFreshState_ModifiedFlagIsNotSet(self):
|
|
state = State(Logger())
|
|
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_GivenFreshState_IsCleanSlate_ReturnsTrue(self):
|
|
state = State(Logger())
|
|
|
|
self.assertTrue(state.is_clean_slate())
|
|
|
|
def test_GivenStateWithSystemIdAssigned_IsCleanSlate_ReturnsFalse(self):
|
|
state = State(Logger())
|
|
state.system_id = "whatever"
|
|
|
|
self.assertFalse(state.is_clean_slate())
|
|
|
|
def test_GivenFreshState_SetSystemId_SetsSystemId(self):
|
|
state = State(Logger())
|
|
state.system_id = "booya"
|
|
|
|
self.assertEqual("booya", state.system_id)
|
|
|
|
def test_GivenStateWithSystemId_SetSameSystemId_IsOk(self):
|
|
state = State(Logger())
|
|
state.system_id = "booya"
|
|
state.modified = False
|
|
state.system_id = "booya"
|
|
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_GivenStateWithSystemId_SetDifferentSystemId_RaisesException(self):
|
|
state = State(Logger())
|
|
state.system_id = "booya"
|
|
state.modified = False
|
|
|
|
with self.assertRaises(SystemIdChanged) as context:
|
|
state.system_id = "baayo"
|
|
self.assertIn("from=booya, to=baayo", str(context.exception))
|
|
|
|
def test_GivenFreshState_SetTimelineId_SetsTimelineId(self):
|
|
state = State(Logger())
|
|
state.timeline_id = 666
|
|
|
|
self.assertEqual(666, state.timeline_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_WithNonIntegerInput_SetTimelineId_RaisesException(self):
|
|
state = State(Logger())
|
|
with self.assertRaises(InvalidTimelineId) as context:
|
|
state.timeline_id = "one hundred"
|
|
self.assertIn("invalid value=one hundred", str(context.exception))
|
|
|
|
def test_GivenStateWithTimelineId_SetLowerTimelineId_RaisesException(self):
|
|
state = State(Logger())
|
|
state.timeline_id = "50"
|
|
|
|
with self.assertRaises(TimelineIdLowered) as context:
|
|
state.timeline_id = 49
|
|
self.assertIn("(from=50, to=49)", str(context.exception))
|
|
|
|
def test_GivenStateWithTimelineId_SetSameTimelineId_IsOk(self):
|
|
state = State(Logger())
|
|
state.timeline_id = 50
|
|
state.modified = False
|
|
|
|
state.timeline_id = "50"
|
|
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_GivenStateWithTimelineId_SetHigherTimelineId_IsOk(self):
|
|
state = State(Logger())
|
|
state.timeline_id = 50
|
|
state.modified = False
|
|
|
|
state.timeline_id = "51"
|
|
|
|
self.assertEqual(51, state.timeline_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_SetLeaderStatus_SetsLeaderStatus(self):
|
|
state = State(Logger())
|
|
state.modified = False
|
|
|
|
state.leader_status = LEADER_CONNECTED
|
|
|
|
self.assertEqual(LEADER_CONNECTED, state.leader_status)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_SetLeaderStatus_ToNonExistentStatus_RaisesException(self):
|
|
state = State(Logger())
|
|
state.modified = False
|
|
|
|
with self.assertRaises(InvalidLeaderStatus) as context:
|
|
state.leader_status = "gosh"
|
|
self.assertIn("'gosh'", str(context.exception))
|
|
|
|
def test_SetLeaderError_SetsLeaderError(self):
|
|
state = State(Logger())
|
|
state.modified = False
|
|
|
|
state.leader_error = "God disappeared in a puff of logic"
|
|
|
|
self.assertEqual("God disappeared in a puff of logic", state.leader_error)
|
|
self.assertTrue(state.modified)
|
|
|
|
state.modified = False
|
|
state.leader_error = None
|
|
|
|
self.assertEqual(None, state.leader_error)
|
|
self.assertTrue(state.modified)
|
|
|
|
|
|
class NodeCollectionTests(unittest.TestCase):
|
|
def test_WithNodeNotYetInState_AddNode_AddsNodeState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
|
|
self.assertEqual(1, node.node_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_WithNodeAlreadyInState_AddNode_RaisesException(self):
|
|
state = State(Logger())
|
|
state.add_node(NodeConfig(123))
|
|
|
|
with self.assertRaises(DuplicateNodeAdded) as context:
|
|
state.add_node(NodeConfig(123))
|
|
self.assertIn("duplicate node_id=123", str(context.exception))
|
|
|
|
def test_WithNodeNotInState_getNode_RaisesException(self):
|
|
state = State(Logger())
|
|
|
|
with self.assertRaises(UnknownNodeRequested):
|
|
state.get_node("that does not exist")
|
|
|
|
def test_WithNodeInState_getNode_ReturnsNode(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig("that does exist"))
|
|
|
|
node_from_state = state.get_node("that does exist")
|
|
|
|
self.assertEqual(node, node_from_state)
|
|
|
|
def test_WithUnknownNode_SetLeaderNode_RaisesException(self):
|
|
state1 = State(Logger())
|
|
state2 = State(Logger())
|
|
node = state2.add_node(NodeConfig(1337))
|
|
with self.assertRaises(UnknownNodeRequested) as context:
|
|
state1.promote_node(node)
|
|
self.assertIn("unknown node_id=1337", str(context.exception))
|
|
|
|
def test_WithNodeWithoutSystemId_SetLeaderNode_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
node.timeline_id = 1
|
|
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
with self.assertRaises(NodeCannotBePromoted) as context:
|
|
node.promote()
|
|
self.assertIn("Node '1'", str(context.exception))
|
|
self.assertIn("node has no system_id", str(context.exception))
|
|
|
|
def test_WithNodeWithoutTimelineId_SetLeaderNode_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
node.system_id = "system"
|
|
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
with self.assertRaises(NodeCannotBePromoted) as context:
|
|
node.promote()
|
|
self.assertIn("Node '1'", str(context.exception))
|
|
self.assertIn("node has no timeline_id", str(context.exception))
|
|
|
|
def test_WithNodeWithoutPgbouncerConfig_SetLeaderNode_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
node.system_id = "a7d8a9347df789saghdfs"
|
|
node.timeline_id = 11111111111
|
|
with self.assertRaises(NodeCannotBePromoted) as context:
|
|
node.promote()
|
|
self.assertIn("Node '1'", str(context.exception))
|
|
self.assertIn("node has no pgbouncer_config", str(context.exception))
|
|
|
|
def test_SetLeaderNode_SetsLeaderNode_WithUnknownLeaderStatus(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node.system_id = "SuperCluster"
|
|
node.timeline_id = " 005 "
|
|
node.promote()
|
|
|
|
self.assertEqual("SuperCluster", state.system_id)
|
|
self.assertEqual(5, state.timeline_id)
|
|
self.assertEqual(1, state.leader_node_id)
|
|
self.assertEqual(LEADER_UNKNOWN, state.leader_status)
|
|
self.assertEqual(None, state.leader_error)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_SetLeaderNode_ToSameLeader_ResetsLeaderNode_WithUnknownLeaderStatus(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node.system_id = "1.2.3.4.5.6.7.8.9.10.11"
|
|
node.timeline_id = 12
|
|
node.promote()
|
|
state.leader_status = LEADER_CONNECTED
|
|
state.leader_error = "Just testin'!"
|
|
state.modified = False
|
|
|
|
node.promote()
|
|
self.assertEqual(None, state.leader_error)
|
|
self.assertEqual(LEADER_UNKNOWN, state.leader_status)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_SetLeaderNode_ToNodeWithDifferentSystemId_RaisesException(self):
|
|
state = State(Logger())
|
|
node1 = state.add_node(NodeConfig(1))
|
|
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node1.system_id = "systemA"
|
|
node1.timeline_id = 10
|
|
node2 = state.add_node(NodeConfig(2))
|
|
node2.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node2.system_id = "systemB"
|
|
node2.timeline_id = 10
|
|
node1.promote()
|
|
|
|
with self.assertRaises(SystemIdChanged):
|
|
node2.promote()
|
|
|
|
def test_SetLeaderNode_ToNodeWithLowerTimelineId_RaisesException(self):
|
|
state = State(Logger())
|
|
node1 = state.add_node(NodeConfig(1))
|
|
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node1.system_id = "systemX"
|
|
node1.timeline_id = 10
|
|
node2 = state.add_node(NodeConfig(2))
|
|
node2.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node2.system_id = "systemX"
|
|
node2.timeline_id = 9
|
|
node1.promote()
|
|
|
|
with self.assertRaises(TimelineIdLowered):
|
|
node2.promote()
|
|
|
|
def test_SetLeaderNode_ToNodeWithSameOrHigherTimelineId_IsOk(self):
|
|
state = State(Logger())
|
|
node1 = state.add_node(NodeConfig(1))
|
|
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node1.system_id = "systemX"
|
|
node1.timeline_id = 42
|
|
node2 = state.add_node(NodeConfig(2))
|
|
node2.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node2.system_id = "systemX"
|
|
node2.timeline_id = 42
|
|
node3 = state.add_node(NodeConfig(3))
|
|
node3.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node3.system_id = "systemX"
|
|
node3.timeline_id = 43
|
|
state.modified = False
|
|
|
|
node1.promote()
|
|
self.assertEqual(1, state.leader_node_id)
|
|
self.assertEqual(42, state.timeline_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
state.modified = False
|
|
node2.promote()
|
|
self.assertEqual(2, state.leader_node_id)
|
|
self.assertEqual(42, state.timeline_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
state.modified = False
|
|
node3.promote()
|
|
self.assertEqual(3, state.leader_node_id)
|
|
self.assertEqual(43, state.timeline_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
|
|
class NodeTests(unittest.TestCase):
|
|
def test_WithNoneValue_SetSystemId_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig("break me"))
|
|
state.modified = False
|
|
|
|
with self.assertRaises(InvalidSystemId) as context:
|
|
node.system_id = None
|
|
self.assertIn("invalid system_id=None", str(context.exception))
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_WithEmptyString_SetSystemId_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig("break me"))
|
|
state.modified = False
|
|
|
|
with self.assertRaises(InvalidSystemId) as context:
|
|
node.system_id = " \t\r\n\t "
|
|
self.assertIn(r"invalid system_id=' \t\r\n\t '", str(context.exception))
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
state.modified = False
|
|
|
|
node.system_id = "X"
|
|
|
|
self.assertEqual("X", node.system_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_WithNonIntegerInput_SetTimelineId_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig("break me"))
|
|
|
|
with self.assertRaises(InvalidTimelineId) as context:
|
|
node.timeline_id = "TARDIS"
|
|
self.assertIn("invalid value=TARDIS", str(context.exception))
|
|
|
|
def test_SetTimelineId_SetsTimelineId_AndNotifiesChangeToState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
state.modified = False
|
|
|
|
node.timeline_id = 25
|
|
|
|
self.assertEqual(25, node.timeline_id)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_SetStatus_SetsStatus_AndNotifiesChangeToState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
state.modified = False
|
|
|
|
node.status = NODE_PRIMARY
|
|
|
|
self.assertEqual(NODE_PRIMARY, node.status)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_WithInvalidStatus_SetStatus_RaisesException(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
state.modified = False
|
|
|
|
with self.assertRaises(InvalidNodeStatus) as context:
|
|
node.status = "DERAILED"
|
|
self.assertIn("'DERAILED'", str(context.exception))
|
|
self.assertFalse(state.modified)
|
|
|
|
def test_SetError_ToString_SetsError_AndNotifiesChangeToState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
state.modified = False
|
|
|
|
node.error = "Found some spare bits at IKEA.py line 141"
|
|
|
|
self.assertEqual("Found some spare bits at IKEA.py line 141", node.error)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_SetError_ToNone_ClearsError_AndNotifiesChangeToState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig(1))
|
|
node.error = "Mouse lost its ball"
|
|
state.modified = False
|
|
|
|
node.error = None
|
|
|
|
self.assertEqual(None, node.error)
|
|
self.assertTrue(state.modified)
|
|
|
|
def test_WhenNothingChanges_NoChangeIsNotifiedToState(self):
|
|
state = State(Logger())
|
|
node = state.add_node(NodeConfig("x"))
|
|
node.system_id = "aaaaaaa"
|
|
node.timeline_id = 55
|
|
node.status = NODE_PRIMARY
|
|
node.error = "Just testin'"
|
|
state.modified = False
|
|
|
|
node.system_id = "aaaaaaa"
|
|
node.timeline_id = 55
|
|
node.status = NODE_PRIMARY
|
|
node.error = "Just testin'"
|
|
|
|
self.assertFalse(state.modified)
|
|
|
|
|
|
class StateExportTests(unittest.TestCase):
|
|
|
|
def test_GivenFilledState_Export_ExportsDataForStore(self):
|
|
self.maxDiff = 5000;
|
|
state = State(Logger())
|
|
|
|
node1 = state.add_node(NodeConfig(1))
|
|
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node1.system_id = "System X"
|
|
node1.timeline_id = 555
|
|
node1.status = NODE_PRIMARY
|
|
node1.error = "Some error for 1"
|
|
|
|
node2 = state.add_node(NodeConfig(2))
|
|
node2.config.pgbouncer_config = PGBOUNCER_CONFIG
|
|
node2.system_id = "System Y"
|
|
node2.timeline_id = 1
|
|
node2.status = NODE_STANDBY
|
|
node2.error = "Some error for 2"
|
|
|
|
node1.promote()
|
|
|
|
state.leader_status = LEADER_CONNECTED
|
|
state.leader_error = "Some error for leader connection"
|
|
|
|
export = state.export()
|
|
|
|
self.assertEqual({
|
|
"system_id": "System X",
|
|
"timeline_id": 555,
|
|
"active_pgbouncer_config": PGBOUNCER_CONFIG,
|
|
"leader_node_id": 1,
|
|
"leader_status": LEADER_CONNECTED,
|
|
"leader_error": "Some error for leader connection",
|
|
"nodes": {
|
|
1: {
|
|
"node_id": 1,
|
|
"config": {
|
|
"pgbouncer_config": PGBOUNCER_CONFIG,
|
|
"host": None,
|
|
"port": None
|
|
},
|
|
"system_id": "System X",
|
|
"timeline_id": 555,
|
|
"is_leader": True,
|
|
"status": NODE_PRIMARY,
|
|
"error": "Some error for 1"
|
|
},
|
|
2: {
|
|
"node_id": 2,
|
|
"config": {
|
|
"pgbouncer_config": PGBOUNCER_CONFIG,
|
|
"host": None,
|
|
"port": None
|
|
},
|
|
"system_id": "System Y",
|
|
"timeline_id": 1,
|
|
"is_leader": False,
|
|
"status": NODE_STANDBY,
|
|
"error": "Some error for 2"
|
|
}
|
|
}
|
|
}, export)
|
|
|