示例#1
0
class TestConsensus:
    @pytest.mark.asyncio
    async def test_term_starts_at_0(self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)

        assert self.raft.term == 0

    @pytest.mark.asyncio
    async def test_state_starts_as_follower(self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)

        assert self.raft.state == PeerState.FOLLOWER

    @pytest.mark.asyncio
    async def test_get_timeout_returns_valid_default_timeout_value(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)

        t = self.raft.get_timeout()
        assert 0.150 <= t <= 0.300

    @pytest.mark.asyncio
    async def test_get_timeout_returns_valid_custom_timeout_value(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator,
                                  self.registry,
                                  min_timeout=0.50,
                                  max_timeout=0.60)

        t = self.raft.get_timeout()
        assert 0.50 <= t <= 0.60

    @pytest.mark.asyncio
    async def test_no_heartbeat_follower_becomes_candidate(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)
        await self.raft.process_state()

        assert self.raft.term == 1
        assert self.raft.state == PeerState.CANDIDATE

    @pytest.mark.asyncio
    async def test_no_heartbeat_follower_becomes_candidate_high_term(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)
        self.raft.term = 8
        await self.raft.process_state()

        assert self.raft.term == 9
        assert self.raft.state == PeerState.CANDIDATE

    @pytest.mark.asyncio
    async def test_heartbeat_follower_stays_follower(self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)
        self.raft.got_heartbeat.set()
        await self.raft.process_state()

        assert self.raft.state == PeerState.FOLLOWER

    @pytest.mark.asyncio
    async def test_valid_heartbeat_sets_got_heartbeat(self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)

        data = {'identifier': '1', 'term': self.raft.term}

        self.raft.on_heartbeat(data)
        assert self.raft.got_heartbeat.is_set()

    @pytest.mark.asyncio
    async def test_valid_heartbeat_sets_got_heartbeat_and_term_updated(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)

        data = {'identifier': '1', 'term': self.raft.term + 1}

        self.raft.on_heartbeat(data)

        assert self.raft.got_heartbeat.is_set()
        assert self.raft.term == data.get('term')

    @pytest.mark.asyncio
    async def test_valid_heartbeat_newer_term_demotes_leader_to_follower(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)
        self.raft.state = PeerState.LEADER

        data = {'identifier': '1', 'term': self.raft.term + 1}

        self.raft.on_heartbeat(data)

        assert self.raft.state == PeerState.FOLLOWER

    @pytest.mark.asyncio
    async def test_valid_heartbeat_newer_term_demotes_candidate_to_follower(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)
        self.raft.state = PeerState.CANDIDATE

        data = {'identifier': '1', 'term': self.raft.term + 1}

        self.raft.on_heartbeat(data)

        assert self.raft.state == PeerState.FOLLOWER

    @pytest.mark.asyncio
    async def test_process_state_resets_heatbeat_flag(self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.registry = Registry([])
        self.raft = RaftConsensus(self.communicator, self.registry)

        data = {'identifier': '1', 'term': self.raft.term}

        self.raft.on_heartbeat(data)
        assert self.raft.got_heartbeat.is_set()
        await self.raft.process_state()
        assert self.raft.got_heartbeat.is_set() is False

    @pytest.mark.asyncio
    async def test_leader_sets_followers_heartbeat_flag(
            self, unused_tcp_port_factory):
        port_l, port_f = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_l = HTTPCommunicator('leader', port_l)
        self.communicator_f = HTTPCommunicator('follower', port_f)
        await self.communicator_f.start()
        self.raft_l = RaftConsensus(
            self.communicator_l,
            Registry([{
                'host': 'localhost',
                'port': port_f,
                'identifier': 'follower'
            }]))
        self.raft_f = RaftConsensus(
            self.communicator_f,
            Registry([{
                'host': 'localhost',
                'port': port_l,
                'identifier': 'leader'
            }]))

        self.raft_l.state = PeerState.LEADER

        assert self.raft_f.got_heartbeat.is_set() is False
        await self.raft_l.process_state()
        assert self.raft_f.got_heartbeat.is_set()
        await self.raft_f.process_state()
        assert self.raft_f.got_heartbeat.is_set() is False

    @pytest.mark.asyncio
    async def test_leader_gives_up_on_followers_long_heartbeat(
            self, unused_tcp_port_factory):
        port_l, port_f = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_l = HTTPCommunicator('leader', port_l)
        self.communicator_f = HTTPCommunicator('follower', port_f)
        await self.communicator_f.start()
        self.raft_l = RaftConsensus(
            self.communicator_l,
            Registry([{
                'host': 'localhost',
                'port': port_f,
                'identifier': 'follower'
            }]))
        self.raft_f = RaftConsensus(
            self.communicator_f,
            Registry([{
                'host': 'localhost',
                'port': port_l,
                'identifier': 'leader'
            }]))
        self.raft_l.state = PeerState.LEADER

        async def long_call(data):
            await asyncio.sleep(0.5)

        self.communicator_f.set_on_heartbeat(long_call)

        t_start = time.time()
        await self.raft_l.process_state()
        duration = time.time() - t_start - 0.050

        assert duration <= 0.15

    @pytest.mark.asyncio
    async def test_candidate_sends_votes(self, unused_tcp_port_factory):
        port_c, port_f = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_c = HTTPCommunicator('candidate', port_c)
        self.communicator_f = HTTPCommunicator('follower', port_f)
        await self.communicator_f.start()
        self.raft_c = RaftConsensus(
            self.communicator_c,
            Registry([{
                'host': 'localhost',
                'port': port_f,
                'identifier': 'follower'
            }]))
        self.raft_f = RaftConsensus(
            self.communicator_f,
            Registry([{
                'host': 'localhost',
                'port': port_c,
                'identifier': 'candidate'
            }]))

        self.raft_c.state = PeerState.CANDIDATE
        self.raft_c.term = 4

        on_rv = Mock()
        on_rv.return_value = True, {"vote_granted": True}
        self.communicator_f.set_on_request_vote(on_rv)

        await self.raft_c.process_state()

        on_rv.assert_called_with({'identifier': 'candidate', 'term': 4})

    @pytest.mark.asyncio
    async def test_valid_heartbeat_cancels_election(self,
                                                    unused_tcp_port_factory):
        port_c, port_l = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_c = HTTPCommunicator('candidate', port_c)
        self.communicator_l = HTTPCommunicator('leader', port_l)
        await self.communicator_l.start()
        self.raft_c = RaftConsensus(
            self.communicator_c,
            Registry([{
                'host': 'localhost',
                'port': port_l,
                'identifier': 'leader'
            }]))
        self.raft_l = RaftConsensus(
            self.communicator_l,
            Registry([{
                'host': 'localhost',
                'port': port_c,
                'identifier': 'candidate'
            }]))

        self.raft_c.state = PeerState.CANDIDATE
        self.raft_c.term = 4
        self.raft_l.term = 4

        async def rv(data):
            self.raft_c.on_heartbeat({
                'identifier': 'leader',
                'term': self.raft_l.term
            })

        self.communicator_l.set_on_request_vote(rv)
        await self.raft_c.process_state()

        assert self.raft_c.state == PeerState.FOLLOWER

    @pytest.mark.asyncio
    async def test_invalid_heartbeat_does_not_cancel_election(
            self, unused_tcp_port_factory):
        port_c, port_l = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_c = HTTPCommunicator('candidate', port_c)
        self.communicator_l = HTTPCommunicator('leader', port_l)
        await self.communicator_l.start()
        self.raft_c = RaftConsensus(
            self.communicator_c,
            Registry([{
                'host': 'localhost',
                'port': port_l,
                'identifier': 'leader'
            }]))
        self.raft_l = RaftConsensus(
            self.communicator_l,
            Registry([{
                'host': 'localhost',
                'port': port_c,
                'identifier': 'candidate'
            }]))

        self.raft_c.state = PeerState.CANDIDATE
        self.raft_c.term = 4
        self.raft_l.term = 4

        async def rv(data):
            self.raft_c.on_heartbeat({
                'identifier': 'leader',
                'term': self.raft_c.term - 1
            })

        self.communicator_l.set_on_request_vote(rv)
        await self.raft_c.process_state()

        assert self.raft_c.state != PeerState.FOLLOWER

    @pytest.mark.asyncio
    async def test_candidate_becomes_leader_after_majority_votes(
            self, unused_tcp_port_factory):
        port_a, port_b, port_c, port_d = unused_tcp_port_factory(
        ), unused_tcp_port_factory(), unused_tcp_port_factory(
        ), unused_tcp_port_factory()
        self.communicator_a = HTTPCommunicator('a', port_a)
        self.communicator_b = HTTPCommunicator('b', port_b)
        self.communicator_c = HTTPCommunicator('c', port_c)
        self.communicator_d = HTTPCommunicator('d', port_d)
        self.raft_a = RaftConsensus(
            self.communicator_a,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_b,
                    'identifier': 'b'
                },
                {
                    'host': 'localhost',
                    'port': port_c,
                    'identifier': 'c'
                },
                {
                    'host': 'localhost',
                    'port': port_d,
                    'identifier': 'd'
                },
            ]))
        self.raft_b = RaftConsensus(
            self.communicator_b,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_a,
                    'identifier': 'a'
                },
                {
                    'host': 'localhost',
                    'port': port_c,
                    'identifier': 'c'
                },
                {
                    'host': 'localhost',
                    'port': port_d,
                    'identifier': 'd'
                },
            ]))
        self.raft_c = RaftConsensus(
            self.communicator_c,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_a,
                    'identifier': 'a'
                },
                {
                    'host': 'localhost',
                    'port': port_b,
                    'identifier': 'b'
                },
                {
                    'host': 'localhost',
                    'port': port_d,
                    'identifier': 'd'
                },
            ]))
        self.raft_d = RaftConsensus(
            self.communicator_d,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_a,
                    'identifier': 'a'
                },
                {
                    'host': 'localhost',
                    'port': port_b,
                    'identifier': 'b'
                },
                {
                    'host': 'localhost',
                    'port': port_c,
                    'identifier': 'c'
                },
            ]))
        await self.communicator_a.start()
        await self.communicator_b.start()
        await self.communicator_c.start()
        await self.communicator_d.start()

        self.raft_a.state = PeerState.CANDIDATE

        def on_rv(data):
            return True, {"vote_granted": True}

        self.communicator_b.set_on_request_vote(on_rv)
        self.communicator_c.set_on_request_vote(on_rv)
        self.communicator_d.set_on_request_vote(on_rv)

        await self.raft_a.process_state()
        assert self.raft_a.state == PeerState.LEADER

    @pytest.mark.asyncio
    async def test_candidate_retries_election_on_split_votes(
            self, unused_tcp_port_factory):
        port_a, port_b, port_c, port_d = unused_tcp_port_factory(
        ), unused_tcp_port_factory(), unused_tcp_port_factory(
        ), unused_tcp_port_factory()
        self.communicator_a = HTTPCommunicator('a', port_a)
        self.communicator_b = HTTPCommunicator('b', port_b)
        self.communicator_c = HTTPCommunicator('c', port_c)
        self.communicator_d = HTTPCommunicator('d', port_d)
        self.raft_a = RaftConsensus(
            self.communicator_a,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_b,
                    'identifier': 'b'
                },
                {
                    'host': 'localhost',
                    'port': port_c,
                    'identifier': 'c'
                },
                {
                    'host': 'localhost',
                    'port': port_d,
                    'identifier': 'd'
                },
            ]))
        self.raft_b = RaftConsensus(
            self.communicator_b,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_a,
                    'identifier': 'a'
                },
                {
                    'host': 'localhost',
                    'port': port_c,
                    'identifier': 'c'
                },
                {
                    'host': 'localhost',
                    'port': port_d,
                    'identifier': 'd'
                },
            ]))
        self.raft_c = RaftConsensus(
            self.communicator_c,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_a,
                    'identifier': 'a'
                },
                {
                    'host': 'localhost',
                    'port': port_b,
                    'identifier': 'b'
                },
                {
                    'host': 'localhost',
                    'port': port_d,
                    'identifier': 'd'
                },
            ]))
        self.raft_d = RaftConsensus(
            self.communicator_d,
            Registry([
                {
                    'host': 'localhost',
                    'port': port_a,
                    'identifier': 'a'
                },
                {
                    'host': 'localhost',
                    'port': port_b,
                    'identifier': 'b'
                },
                {
                    'host': 'localhost',
                    'port': port_c,
                    'identifier': 'c'
                },
            ]))
        await self.communicator_a.start()
        await self.communicator_b.start()
        await self.communicator_c.start()
        await self.communicator_d.start()

        self.raft_a.state = PeerState.CANDIDATE

        def on_rv_true(data):
            return True, {"vote_granted": True}

        def on_rv_false(data):
            return True, {"vote_granted": False}

        self.communicator_b.set_on_request_vote(on_rv_true)
        self.communicator_c.set_on_request_vote(on_rv_false)
        self.communicator_d.set_on_request_vote(on_rv_false)

        await self.raft_a.process_state()
        assert self.raft_a.state == PeerState.CANDIDATE

    @pytest.mark.asyncio
    async def test_old_candidate_rejected_by_newer_term(
            self, unused_tcp_port_factory):
        port_c, port_f = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_c = HTTPCommunicator('candidate', port_c)
        self.communicator_f = HTTPCommunicator('leader', port_f)
        await self.communicator_f.start()
        self.raft_c = RaftConsensus(
            self.communicator_c,
            Registry([{
                'host': 'localhost',
                'port': port_f,
                'identifier': 'follower'
            }]))
        self.raft_f = RaftConsensus(
            self.communicator_f,
            Registry([{
                'host': 'localhost',
                'port': port_c,
                'identifier': 'candidate'
            }]))

        self.raft_c.state = PeerState.CANDIDATE
        self.raft_c.term = 3
        self.raft_f.term = 4

        await self.raft_c.process_state()

        assert self.raft_c.state == PeerState.CANDIDATE

    @pytest.mark.asyncio
    async def test_candidate_only_votes_once(self, unused_tcp_port_factory):
        port_1, port_2 = unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_1 = HTTPCommunicator('candidate_1', port_1)
        self.communicator_2 = HTTPCommunicator('candidate_2', port_2)
        await self.communicator_1.start()
        await self.communicator_2.start()
        self.raft_1 = RaftConsensus(
            self.communicator_1,
            Registry([{
                'host': 'localhost',
                'port': port_1,
                'identifier': 'candidate_1'
            }]))
        self.raft_2 = RaftConsensus(
            self.communicator_2,
            Registry([{
                'host': 'localhost',
                'port': port_2,
                'identifier': 'candidate_2'
            }]))

        self.raft_1.state = PeerState.CANDIDATE
        self.raft_2.state = PeerState.CANDIDATE

        async def rv():
            return True, {"vote_granted": True}

        self.communicator_2.set_on_request_vote(rv)

        await self.raft_1.process_state()
        await self.raft_2.process_state()

        assert self.raft_1.state == PeerState.CANDIDATE
        assert self.raft_2.state == PeerState.CANDIDATE

    @pytest.mark.asyncio
    async def test_follower_will_vote_for_newer_term(self,
                                                     unused_tcp_port_factory):
        port_1, port_2, port_3 = unused_tcp_port_factory(
        ), unused_tcp_port_factory(), unused_tcp_port_factory()
        self.communicator_1 = HTTPCommunicator('candidate_1', port_1)
        self.communicator_2 = HTTPCommunicator('candidate_2', port_2)
        self.communicator_3 = HTTPCommunicator('candidate_3', port_3)
        await self.communicator_1.start()
        await self.communicator_2.start()
        await self.communicator_3.start()
        self.raft_1 = RaftConsensus(
            self.communicator_1,
            Registry([{
                'host': 'localhost',
                'port': port_1,
                'identifier': 'candidate_1'
            }]))
        self.raft_2 = RaftConsensus(
            self.communicator_2,
            Registry([{
                'host': 'localhost',
                'port': port_2,
                'identifier': 'candidate_2'
            }]))
        self.raft_3 = RaftConsensus(
            self.communicator_3,
            Registry([{
                'host': 'localhost',
                'port': port_3,
                'identifier': 'candidate_3'
            }]))

        self.raft_1.term = 87
        self.raft_1.state = PeerState.CANDIDATE
        self.raft_1.has_voted_in_term = True

        self.raft_2.term = 87
        self.raft_2.state = PeerState.CANDIDATE
        self.raft_2.has_voted_in_term = True

        self.raft_3.term = 86
        self.raft_3.state = PeerState.FOLLOWER

        # Simulate raft_1 sending a request vote
        data_1 = {"identifier": "candidate_1", "term": self.raft_1.term}
        assert self.raft_2.on_request_vote(data_1) == (True, {
            "vote_granted": False
        })
        assert self.raft_3.on_request_vote(data_1) == (True, {
            "vote_granted": True
        })
        assert self.raft_3.term == 87
        assert self.raft_3.state == PeerState.FOLLOWER
        assert self.raft_3.has_voted_in_term

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_non_tuple(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = None
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_incorrect_tuple_fields(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = None, False, 5
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_incorrect_first_field(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = None, {}
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_false_first_field(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = False, {}
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_incorrect_second_field(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = True, 4
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_true_with_ballot_vote(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = True, {'vote_granted': True}
        assert self.raft.parse_ballot(ballot)

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_ballot_no_vote(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = None, {'vote_granted': False}
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_parse_ballot_returns_false_with_missing_ballot_data(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        ballot = None, {'garbage': True}
        assert self.raft.parse_ballot(ballot) is False

    @pytest.mark.asyncio
    async def test_is_leader_returns_true_when_leader(self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        self.raft.state = PeerState.LEADER
        assert self.raft.is_leader()

    @pytest.mark.asyncio
    async def test_is_leader_returns_false_when_not_leader(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        self.raft.state = PeerState.FOLLOWER
        assert self.raft.is_leader() is False
        self.raft.state = PeerState.CANDIDATE
        assert self.raft.is_leader() is False

    @pytest.mark.asyncio
    async def test_is_follower_returns_true_when_follower(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        self.raft.state = PeerState.FOLLOWER
        assert self.raft.is_follower()

    @pytest.mark.asyncio
    async def test_is_follower_returns_false_when_not_folower(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        self.raft.state = PeerState.LEADER
        assert self.raft.is_follower() is False
        self.raft.state = PeerState.CANDIDATE
        assert self.raft.is_follower() is False

    @pytest.mark.asyncio
    async def test_is_candidate_returns_true_when_candidate(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        self.raft.state = PeerState.CANDIDATE
        assert self.raft.is_candidate()

    @pytest.mark.asyncio
    async def test_is_candidate_returns_false_when_not_candidate(
            self, unused_tcp_port):
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        self.raft = RaftConsensus(self.communicator, Registry([]))
        self.raft.state = PeerState.FOLLOWER
        assert self.raft.is_candidate() is False
        self.raft.state = PeerState.LEADER
        assert self.raft.is_candidate() is False
示例#2
0
class TestHTTPCommunicator:
    @pytest.mark.asyncio
    async def test_creates_requestor(self):
        """Test that a requester is initialized"""
        # Setup
        self.communicator = HTTPCommunicator('a', 7000)

        # Assert
        assert self.communicator._requester is not None

    @pytest.mark.asyncio
    async def test_creates_responder(self):
        """Test that a responder is initialized"""
        # Setup
        self.communicator = HTTPCommunicator('a', 7000)

        # Assert
        assert self.communicator._responder is not None

    @pytest.mark.asyncio
    async def test_ping_returns_true(self, unused_tcp_port):
        """Test that a ping comes back"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()

        # Act
        result = await self.communicator.ping('localhost', unused_tcp_port)

        # Assert
        assert result

    @pytest.mark.asyncio
    async def test_ping_returns_false(self, unused_tcp_port):
        """Test that a ping doesn't come back"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()

        # Act
        result = await self.communicator.ping('localhost', 0)

        # Assert
        assert result is False

    @pytest.mark.asyncio
    async def test_ping_timeout(self, unused_tcp_port):
        """Test that a ping will timeout"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()

        # Act
        status = await self.communicator.ping('localhost',
                                              unused_tcp_port,
                                              timeout=0)

        # Assert
        assert status is False

    @pytest.mark.asyncio
    async def test_heartbeat_returns_true(self):
        """Test that sending a heartbeat returns with response code 200"""
        # Setup
        self.communicator = HTTPCommunicator('a', 7000)
        await self.communicator.start()

        # Act
        status, data = await self.communicator.send_heartbeat(
            'localhost', 7000, {'identifier': 'a'})

        # Assert
        assert status is True
        assert data is None

    @pytest.mark.asyncio
    async def test_heartbeat_returns_false_on_error(self, unused_tcp_port):
        """Test that nothing returns on a connection error"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()

        # Act
        status, data = await self.communicator.send_heartbeat(
            'localhost', 0, {})

        # Assert
        assert status is False
        assert data is None

    @pytest.mark.asyncio
    async def test_on_heartbeat_executes(self, unused_tcp_port):
        """Test that our function is called each time we receive a heartbeat"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        on_heartbeat = Mock()
        on_heartbeat.return_value = True, {}
        self.communicator.set_on_heartbeat(on_heartbeat)
        data = {'identifier': 'a', 'term': 1}

        # Act
        await self.communicator.send_heartbeat('localhost', unused_tcp_port,
                                               data)

        # Assert
        on_heartbeat.assert_called_with(data)

    @pytest.mark.asyncio
    async def test_on_heartbeat_can_return_error(self, unused_tcp_port):
        """Test that our function can cause an error to be returned"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        on_heartbeat = Mock()
        on_heartbeat.return_value = (False, {"status", "error!"})
        self.communicator.set_on_heartbeat(on_heartbeat)

        # Act
        status, data = await self.communicator.send_heartbeat(
            'localhost', unused_tcp_port, {})

        # Assert
        assert status is False
        assert data is None

    @pytest.mark.asyncio
    async def test_on_heartbeat_rejects_invalid_data(self, unused_tcp_port):
        """Test that the data parameter rejects non None or dict data."""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()

        # Assert
        with pytest.raises(ValueError):
            await self.communicator.send_heartbeat('localhost',
                                                   unused_tcp_port, 5)

    @pytest.mark.asyncio
    async def test_on_heartbeat_transforms_none_data(self, unused_tcp_port):
        """Test that the data parameter is an empty dict when None is supplied"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        cb = Mock()
        self.communicator.set_on_heartbeat(cb)

        # Act
        await self.communicator.send_heartbeat('localhost', unused_tcp_port,
                                               None)

        # Assert
        cb.assert_called_with({})

    @pytest.mark.asyncio
    async def test_heartbeats_timeout_after_500_ms(self, unused_tcp_port):
        """Test that a heartbeat can timeout after 500ms with no response"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)

        async def long_callback(identifier):
            await asyncio.sleep(1)

        self.communicator.set_on_heartbeat(long_callback)
        await self.communicator.start()

        # Act
        status, data = await self.communicator.send_heartbeat('localhost',
                                                              unused_tcp_port,
                                                              {},
                                                              timeout=0.5)

        # Assert
        assert status is False
        assert data is None

    @pytest.mark.asyncio
    async def test_register_returns_true(self, unused_tcp_port):
        """Test that registering returns true."""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()

        # Act
        status = await self.communicator.register_with('localhost',
                                                       unused_tcp_port)

        # Assert
        assert status is True

    @pytest.mark.asyncio
    async def test_on_register_executes(self, unused_tcp_port):
        """Test that our register callback is called with the right data"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        on_register = Mock()
        self.communicator.set_on_register(on_register)

        # Act
        await self.communicator.register_with('localhost', unused_tcp_port)

        # Assert
        on_register.assert_called_with('localhost', unused_tcp_port, 'a')

    @pytest.mark.asyncio
    async def test_on_register_can_return_error(self, unused_tcp_port):
        """Test that our register callback can force an error"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        on_register = Mock()
        on_register.return_value = (False, "Error!")
        self.communicator.set_on_register(on_register)

        # Act
        status = await self.communicator.register_with('localhost',
                                                       unused_tcp_port)

        # Assert
        assert status is False

    @patch.object(aiohttp.web, 'Response')
    def test_respond_text_success(self, web_response):
        """Test that passing a success result with data returns a 200 result
        with text in the body."""
        # Setup
        cb = (True, "Go Hokies!")

        # Act
        _HTTPResponder.respond(cb)

        # Assert
        web_response.assert_called_with(status=200, text="Go Hokies!")

    @patch.object(aiohttp.web, 'Response')
    def test_respond_text_failure(self, web_response):
        """Test that passing a failure result with data returns a 400 result
        with text in the body."""
        # Setup
        cb = (False, "Go Hoos!")

        # Act
        _HTTPResponder.respond(cb)

        # Assert
        web_response.assert_called_with(status=400, text="Go Hoos!")

    @patch.object(aiohttp.web, 'json_response')
    def test_respond_json_success(self, web_response):
        """Test that passing a success result with object data returns a 200
        result with a json result in the body."""
        # Setup
        cb = (True, {"Let's go!": "Hokies!"})

        # Act
        _HTTPResponder.respond(cb)

        # Assert
        web_response.assert_called_with({"Let's go!": "Hokies!"}, status=200)

    @patch.object(aiohttp.web, 'json_response')
    def test_respond_json_failure(self, web_response):
        """Test that passing a success result with object data returns a 400
        result with a json result in the body."""
        # Setup
        cb = (False, {"Boo": "Hoo"})

        # Act
        _HTTPResponder.respond(cb)

        # Assert
        web_response.assert_called_with({"Boo": "Hoo"}, status=400)

    @pytest.mark.asyncio
    async def test_request_vote_returns_400_with_no_callback(
            self, unused_tcp_port):
        """Test that request vote returns a 400 status when no callback is defined"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        self.communicator.set_on_request_vote(None)

        # Act
        res = await self.communicator._requester.post('localhost',
                                                      unused_tcp_port,
                                                      '/raft/request_vote',
                                                      {'test': 123})

        # Assert
        assert res.status == 400

    @pytest.mark.asyncio
    async def test_request_vote_passes_data_to_callback(self, unused_tcp_port):
        """Test that request vote passed the data to the defined callback"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        on_request_vote = Mock()
        self.communicator.set_on_request_vote(on_request_vote)
        data = {'test': True}

        # Act
        await self.communicator.request_vote('localhost', unused_tcp_port,
                                             data)

        # Assert
        on_request_vote.assert_called_with(data)

    @pytest.mark.asyncio
    async def test_request_votes_timeout_after_500_ms(self, unused_tcp_port):
        """Test that request vote will timeout after a given timeout"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)

        async def long_callback(data):
            await asyncio.sleep(1)

        self.communicator.set_on_request_vote(long_callback)
        await self.communicator.start()

        # Act
        status, data = await self.communicator.request_vote('localhost',
                                                            unused_tcp_port,
                                                            {},
                                                            timeout=0.5)

        # Assert
        assert status is False
        assert data is None

    @pytest.mark.asyncio
    async def test_request_vote_returns_on_error(self, unused_tcp_port):
        """Test that request vote will return nothing on error"""
        # Setup
        self.communicator = HTTPCommunicator('a', unused_tcp_port)
        await self.communicator.start()
        on_request_vote = Mock()
        self.communicator.set_on_request_vote(on_request_vote)
        data = {'test': True}

        # Act
        status, data = await self.communicator.request_vote(
            'localhost', 0, data)

        # Assert
        assert status is False
        assert data is None