# -*- 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)