def test_setAndGetCurrentTerm(self): '''getCurrentTerm should return the current term; if the persister is new, the term should be 0. setcurrentTerm should durably set the current term, and it should be retrievable by getCurrentTerm. ''' results = self.persister.getCurrentTerm() zeroFailureMessage = 'getCurrentTerm for new persister should return 0' results.addCallback(self.assertEqual, 0, msg=zeroFailureMessage) results.addCallback( dropResult(self.persister.setCurrentTerm, 123)) setCurrentTermFailureMessage = 'setCurrentTerm did not set the term' results.addCallback(self.assertEqual, 123, msg=setCurrentTermFailureMessage) results.addCallback(dropResult(self.persister.getCurrentTerm)) getCurrentTermFailureMessage = ('getCurrentTerm did not agree' ' with setCurrentTerm') results.addCallback(self.assertEqual, 123, msg=getCurrentTermFailureMessage) return results
def test_votedForAndvoteFor(self): '''votedFor should return the identity of the candidate voted for most recently; if the persister is new, it should return None. voteFor should durably set the identity of the candidate voted for, and that identity should be retrievable by votedFor ''' results = self.persister.votedFor() noneVotedForFailure = 'votedFor for a empty persister should be None' results.addCallback(self.assertEqual, None, msg=noneVotedForFailure) for identity in ('first id', 'second id'): results.addCallback(dropResult(self.persister.voteFor, identity)) voteForFailureMessage = ('voteFor did not correctly set the' ' candidate identity') results.addCallback(self.assertEqual, identity, msg=voteForFailureMessage) results.addCallback(dropResult(self.persister.votedFor)) votedForFailureMessage = 'votedFor disagreed with voteFor' results.addCallback(self.assertEqual, identity, msg=votedForFailureMessage) return results
def test_overlappingMatchAndAppendNewLogEntries(self): '''matchAndAppendNewLogEntries should delete conflicting entries and ignore ones that match. ''' results = defer.succeed(None) for idx in xrange(len(firstEntries)): # drop existing data results.addCallback(dropResult(self.persister._truncateLog)) results.addCallback( dropResult(self.persister.addNewEntries, firstEntries)) # construct overlappingEntries such that: # 1) its 0th item matches the currentLog[idx] # 2) its subsequent items do not match currentLog[idx+1:] # # then ensure that it's inserted at lastIndex = idx - 1 # # overlappingLog: [1][4][5] # currentlog: ...[1][1][2][3]... # ^ ^ # / \ # lastIndex idx # # matchAndAppendNewLogEntries should combine these such # that: # # currentLog: ...[1][1][4][5] lastMatchingIndex = idx - 1 overlappingEntries = [firstEntries[idx]] + appendedEntries expectedEntries = firstEntries[:idx] + overlappingEntries results.addCallback( dropResult(self.persister.matchAndAppendNewLogEntries, lastMatchingIndex, overlappingEntries)) results.addCallback( dropResult(self.persister._logSlice, 0, -1)) def assertMatch(logSlice, idx=idx, overlappingEntries=overlappingEntries, expectedEntries=expectedEntries): failureMessage = ('logSlice disagrees with overlapping' ' matchAndAppendNewLogEntries @ %d' % idx) self.assertEqual(logSlice, expectedEntries, msg=failureMessage) results.addCallback(assertMatch) return results
def closureCallbacks(idx=idx, entry=entry): results.addCallback( dropResult(self.persister.addNewEntries, [entry])) results.addCallback( dropResult(self.persister.lastIndexLETerm, entry.term)) failureMessage = ('lastIndexLETerm returned False for' ' idx %d and term %d' % (idx, entry.term)) results.addCallback(self.assertTrue, msg=failureMessage)
def test__truncateLog(self): '''Sanity test for test crutch _truncateLog. _truncateLog should do just that: truncate the log. ''' results = self.persister.addNewEntries(firstEntries) results.addCallback(dropResult(self.persister._truncateLog)) results.addCallback(dropResult(self.persister._logSlice, 0, -1)) emptyLogFailure = "_truncateLog didn't truncate the log" results.addCallback(self.assertFalse, msg=emptyLogFailure) return results
def test_indexMatchesTerm(self): '''indexMatchesTerm should return True if the log entry's term matches term at the index, or if there are no entries in the log. ''' results = self.persister.indexMatchesTerm(10, 123) trueFailureMessage = ('indexMatchesTerm should return True for' ' an empty persister') results.addCallback(self.assertTrue, msg=trueFailureMessage) results.addCallback( dropResult(self.persister.addNewEntries, firstEntries)) for idx, entry in enumerate(firstEntries): def closureCallbacks(idx=idx, entry=entry): results.addCallback( dropResult(self.persister.indexMatchesTerm, idx, entry.term)) failureMessage = ('indexMatchesTerm returned False for' ' idx %d and term %d' % (idx, entry.term)) results.addCallback(self.assertTrue, msg=failureMessage) closureCallbacks() return results
def closureCallbacks(idx=idx, entry=entry): results.addCallback( dropResult(self.persister.indexMatchesTerm, idx, entry.term)) failureMessage = ('indexMatchesTerm returned False for' ' idx %d and term %d' % (idx, entry.term)) results.addCallback(self.assertTrue, msg=failureMessage)
def closureCallbacks(gte=gte, lt=lt): results.addCallback( dropResult(self.persister._logSlice, gte, lt)) failureMesage = '_logSlice(%d, %d) returned incorrect list' results.addCallback(self.assertEquals, firstEntries[gte:lt], msg=failureMesage)
def test_willBecomeLeader_True(self): self.state.peers = set([1, 2]) results = self.state.willBecomeLeader(votesSoFar=2) msg = 'Should be true with 2 votes' results.addCallback(self.assertFalse, msg=msg) results.addCallback( dropResult(self.assertTrue, isinstance(self.server.state, raft.Leader))) return results
def test_willBecomeLeader_True(self): self.state.peers = set([1, 2]) results = self.state.willBecomeLeader(votesSoFar=2) msg = 'Should be true with 2 votes' results.addCallback(self.assertFalse, msg=msg) results.addCallback(dropResult(self.assertTrue, isinstance(self.server.state, raft.Leader))) return results
def test_disjointMatchAndAppendNewLogEntries(self): '''matchAndAppendNewLogEntries should append disjoint logs. ''' results = self.persister.matchAndAppendNewLogEntries(-1, firstEntries) results.addCallback(dropResult(self.persister.getLastIndex)) indexWrongFailure = ('getLastIndex claims' ' matchAndAppendNewLogEntries' ' failed to append entries') firstEntriesNewIndex = len(firstEntries) - 1 results.addCallback(self.assertEqual, firstEntriesNewIndex, msg=indexWrongFailure) results.addCallback(dropResult(self.persister._logSlice, 0, -1)) logSliceWrongFailure = ('logSlice claims matchAndAppendNewLogEntries' ' failed to append entries') results.addCallback(self.assertEqual, firstEntries, msg=logSliceWrongFailure) results.addCallback( dropResult(self.persister.matchAndAppendNewLogEntries, firstEntriesNewIndex, appendedEntries)) results.addCallback( dropResult(self.persister.getLastIndex)) results.addCallback(self.assertEqual, len(firstEntries) + len(appendedEntries) - 1, msg=indexWrongFailure) results.addCallback( dropResult(self.persister._logSlice, 0, -1)) results.addCallback(self.assertEqual, firstEntries + appendedEntries, msg=logSliceWrongFailure) return results
def test_addNewEntries(self): '''addNewEntries should append the entries to the end of the log without any matching. ''' results = self.persister.addNewEntries(firstEntries) results.addCallback(dropResult(self.persister._logSlice, 0, -1)) entriesFailureMessage = 'addNewEntries failed to add all entries' results.addCallback(self.assertEqual, firstEntries, msg=entriesFailureMessage) results.addCallback(dropResult(self.persister.addNewEntries, appendedEntries)) results.addCallback(dropResult(self.persister._logSlice, 0, -1)) results.addCallback(self.assertEqual, firstEntries + appendedEntries, msg=entriesFailureMessage) return results
def test_getLastTerm(self): '''getLastTerm should return the term of the last log entry; an empty persister's getLastTerm should return 0. ''' results = self.persister.getLastTerm() zeroFailureMessage = "getLastTerm for an empty persister should return 0" results.addCallback(self.assertEqual, 0, msg=zeroFailureMessage) for entries in (firstEntries, appendedEntries): results.addCallback( dropResult(self.persister.addNewEntries, entries)) results.addCallback( dropResult(self.persister.getLastTerm)) failureMessage = 'getLastTerm returned wrong term' results.addCallback(self.assertEqual, entries[-1].term, msg=failureMessage) return results
def test_committableLogEntries(self): '''committableLogEntries should return log entries with indices in the range (lastApplied, commitIndex]. An empty persister has no committable log entries. ''' results = self.persister.committableLogEntries(0, 100) emptyFailureMessage = ('committableLogEntries for an empty' ' persister should return an empty list') results.addCallback(self.assertEqual, [], msg=emptyFailureMessage) results.addCallback( dropResult(self.persister.addNewEntries, firstEntries)) indices = range(-1, len(firstEntries)) for lastApplied, commitIndex in itertools.combinations(indices, 2): results.addCallback( dropResult(self.persister.committableLogEntries, lastApplied, commitIndex)) def assertEntries(result, lastApplied=lastApplied, commitIndex=commitIndex): # convert (,] interval to [,) start, stop = lastApplied + 1, commitIndex + 1 expected = firstEntries[start:stop] failureMessage = ('committableLogEntries returned wrong' ' entries for lastApplied %d and' ' commitIndex %d' % (lastApplied, commitIndex)) self.assertEqual(result, expected, msg=failureMessage) results.addCallback(assertEntries) return results
def test_candidateIdOK(self): """ CandidateIdOK should check whether or not we voted for another candidate. """ ID = 'someID' results = self.state.candidateIdOK(candidateId=ID) candidateIDOKFailureMessage = ("new state's candidateIDOK should" " OK any ID") results.addCallback(self.assertTrue, msg=candidateIDOKFailureMessage) results.addCallback(dropResult(self.state.persister.votedFor)) results.addCallback( dropResult(self.state.persister.voteFor, 'otherId')) results.addCallback( dropResult(self.state.candidateIdOK, candidateId=ID)) candidateIDNotOKFailureMessage = ('candidateIDOK should return' ' False after state has voted') results.addCallback(self.assertFalse, msg=candidateIDNotOKFailureMessage) return results
def test_candidateIdOK(self): """ CandidateIdOK should check whether or not we voted for another candidate. """ ID = 'someID' results = self.state.candidateIdOK(candidateId=ID) candidateIDOKFailureMessage = ("new state's candidateIDOK should" " OK any ID") results.addCallback(self.assertTrue, msg=candidateIDOKFailureMessage) results.addCallback(dropResult(self.state.persister.votedFor)) results.addCallback(dropResult(self.state.persister.voteFor, 'otherId')) results.addCallback( dropResult(self.state.candidateIdOK, candidateId=ID)) candidateIDNotOKFailureMessage = ('candidateIDOK should return' ' False after state has voted') results.addCallback(self.assertFalse, msg=candidateIDNotOKFailureMessage) return results
def test_incrementCurrentTerm(self): '''setCurrentTerm should durably increment the currentTerm and return it. ''' results = defer.succeed(None) for expected in (1, 2, 3): results.addCallback(dropResult(self.persister.setCurrentTerm, None, increment=True)) incrementFailureMessage = ('setCurrentTerm incremented term' ' incorrectly') results.addCallback(self.assertEqual, expected, msg=incrementFailureMessage) results.addCallback(dropResult(self.persister.getCurrentTerm)) getCurrentTermFailureMessage = ('getCurrentTerm disagreed with' ' incremented term') results.addCallback(self.assertEqual, expected, msg=getCurrentTermFailureMessage) return results
def test_getLastIndex(self): '''getLastIndex should return the index of the last log entry; an empty persister's getLastIndex should return -1. ''' results = self.persister.getLastIndex() negOneFailureMessage = ('getLastTerm for an empty persister' ' should return -1') results.addCallback(self.assertEqual, -1, msg=negOneFailureMessage) lastIndex = -1 for entries in (firstEntries, appendedEntries): lastIndex += len(entries) results.addCallback( dropResult(self.persister.addNewEntries, entries)) results.addCallback( dropResult(self.persister.getLastIndex)) failureMessage = 'getLastIndex returned the wrong index' results.addCallback(self.assertEqual, lastIndex, msg=failureMessage) return results
def test__logSlice(self): '''Sanity test for test crutch _logSlice. _logSlice should return log entries between [start, end). ''' results = self.persister._logSlice(0, 1000) falseFailure = '_logSlice on empty persister return non-empty list' results.addCallback(self.assertFalse, msg=falseFailure) results.addCallback(dropResult(self.persister._logSlice, 0, -1)) results.addCallback(self.assertFalse, msg=falseFailure) results.addCallback(dropResult(self.persister.addNewEntries, firstEntries)) for gte, lt in itertools.combinations(range(len(firstEntries)), 2): def closureCallbacks(gte=gte, lt=lt): results.addCallback( dropResult(self.persister._logSlice, gte, lt)) failureMesage = '_logSlice(%d, %d) returned incorrect list' results.addCallback(self.assertEquals, firstEntries[gte:lt], msg=failureMesage) closureCallbacks() results.addCallback(dropResult(self.persister._logSlice, 0, -1)) fullEntriesFailureMessage = '_logSlice(0, -1) returned incorrect list' results.addCallback(self.assertEquals, firstEntries, msg=fullEntriesFailureMessage) return results
def closureCallback(prevLogIndex=prevLogIndex): results.addCallback( dropResult(self.persister.appendEntriesView, prevLogIndex)) def verifyView(view): currentTerm = 0 prevLogTerm = firstEntries[prevLogIndex].term entries = firstEntries[prevLogIndex + 1:] lastLogIndex = len(firstEntries) - 1 expected = persist.AppendEntriesView(currentTerm, lastLogIndex, prevLogTerm, entries) self.assertEqual(view, expected) results.addCallback(verifyView)
def test_appendEntriesView(self): '''appendEntriesView should return a "view" tuple: 1) the currentTerm of the persister, 2) the lastLogIndex of the persister, 3) the prevLogTerm for the log entry at the provided prevLogIndex, 4) a list of entries after prevLogIndex. For an empty persister, these should be their defaults as above. ''' results = self.persister.appendEntriesView(123) emptyExpected = persist.AppendEntriesView(currentTerm=0, lastLogIndex=-1, prevLogTerm=0, entries=[]) emptyFailureMessage = ('appendEntriesView empty persister returned' ' incorrect AppendEntriesView object') results.addCallback(self.assertEqual, emptyExpected, msg=emptyFailureMessage) results.addCallback(dropResult(self.persister.addNewEntries, firstEntries)) for prevLogIndex in xrange(len(firstEntries)): def closureCallback(prevLogIndex=prevLogIndex): results.addCallback( dropResult(self.persister.appendEntriesView, prevLogIndex)) def verifyView(view): currentTerm = 0 prevLogTerm = firstEntries[prevLogIndex].term entries = firstEntries[prevLogIndex + 1:] lastLogIndex = len(firstEntries) - 1 expected = persist.AppendEntriesView(currentTerm, lastLogIndex, prevLogTerm, entries) self.assertEqual(view, expected) results.addCallback(verifyView) closureCallback() return results
def test_requestVote(self): """ Do I give you a vote? It depends. 1) Is your term less than mine? If so, the answer is no. 2) Have I voted for you before, and candidate log is up to date? Then yes """ currentTerm = 100 candidateId = 'ThisCandidate' lastLogIndex = 10 lastLogTerm = currentTerm log = [] for x in xrange(10): log.append(rpc_objects.LogEntry(term=currentTerm, command=x)) results = self.state.persister.setCurrentTerm(currentTerm) results.addCallback( dropResult(self.state.persister.matchAndAppendNewLogEntries, -1, log)) # Test for term less then (case 1) results.addCallback( dropResult(self.state.requestVote, term=currentTerm - 1, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm)) results.addCallback(self.assertEquals, (currentTerm, False)) currentTerm += 1 # Test for success results.addCallback( dropResult(self.state.requestVote, term=currentTerm, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm)) results.addCallback(self.assertEquals, (currentTerm, True)) results.addCallback( dropResult(self.assertTrue, isinstance(self.server.state, raft.Follower))) # Test for failure (voted before, but fell behind) results.addCallback( dropResult(self.state.requestVote, term=currentTerm, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm - 1)) results.addCallback(self.assertEquals, (currentTerm, False)) # Test for failure (already became follower for this term) results.addCallback( dropResult(self.state.requestVote, term=currentTerm, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm)) results.addCallback(self.assertEquals, (currentTerm, False)) return results
def test_candidateLogUpToDate(self): """ Is the candidate up to date? The term and the index together have to be at least as advanced as that of the current machine. """ currentTerm = 100 log = [] for x in xrange(10): log.append(rpc_objects.LogEntry(term=currentTerm, command=x)) results = self.state.persister.setCurrentTerm(currentTerm) results.addCallback( dropResult(self.state.persister.matchAndAppendNewLogEntries, -1, log)) # Index and term are equal results.addCallback( dropResult(self.state.candidateLogUpToDate, lastLogIndex=len(log) - 1, lastLogTerm=currentTerm)) indexAndTermOKFailureMessage = fill( '''candidateLogUpToDate should return True when lastLogIndex and lastLogTerm are equal to the state's last log index and the term of the last log entry.''') results.addCallback(self.assertTrue, msg=indexAndTermOKFailureMessage) results.addCallback( dropResult(self.state.candidateLogUpToDate, lastLogIndex=len(log), lastLogTerm=currentTerm)) indexIsHigherFailureMessage = fill( '''candidateLogUpToDate should return True when lastLogIndex is greater than state's last log index and the lastLogTerm is equal to the term of the last log entry.''') results.addCallback(self.assertTrue, msg=indexIsHigherFailureMessage) results.addCallback( dropResult(self.state.candidateLogUpToDate, lastLogIndex=len(log) - 1, lastLogTerm=currentTerm + 1)) termIsHigherFailureMessage = fill( '''candidateLogUpToDate should return True when lastLogIndex is equal to the state's last log index and the lastLogTerm is greater than the term of the last log entry.''') results.addCallback(self.assertTrue, msg=termIsHigherFailureMessage) results.addCallback( dropResult(self.state.candidateLogUpToDate, lastLogIndex=len(log) - 2, lastLogTerm=currentTerm)) indexIsLowerFailureMessage = fill( '''candidateLogUpToDate should return False when lastLogIndex is less than the state's last log index''') results.addCallback(self.assertFalse, msg=indexIsLowerFailureMessage) # # Term is lower results.addCallback( dropResult(self.state.candidateLogUpToDate, lastLogIndex=len(log), lastLogTerm=currentTerm - 1)) termIsLowerFailureMessage = fill( '''candidateLogUpToDate should return False when lastLogTerm is less than the term of the last log entry.''') results.addCallback(self.assertFalse, msg=termIsLowerFailureMessage) return results
def test_requestVote(self): """ Do I give you a vote? It depends. 1) Is your term less than mine? If so, the answer is no. 2) Have I voted for you before, and candidate log is up to date? Then yes """ currentTerm = 100 candidateId = 'ThisCandidate' lastLogIndex = 10 lastLogTerm = currentTerm log = [] for x in xrange(10): log.append(rpc_objects.LogEntry(term=currentTerm, command=x)) results = self.state.persister.setCurrentTerm(currentTerm) results.addCallback( dropResult(self.state.persister.matchAndAppendNewLogEntries, -1, log)) # Test for term less then (case 1) results.addCallback( dropResult(self.state.requestVote, term=currentTerm - 1, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm)) results.addCallback(self.assertEquals, (currentTerm, False)) currentTerm += 1 # Test for success results.addCallback( dropResult(self.state.requestVote, term=currentTerm, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm)) results.addCallback(self.assertEquals, (currentTerm, True)) results.addCallback(dropResult(self.assertTrue, isinstance(self.server.state, raft.Follower))) # Test for failure (voted before, but fell behind) results.addCallback( dropResult(self.state.requestVote, term=currentTerm, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm - 1)) results.addCallback(self.assertEquals, (currentTerm, False)) # Test for failure (already became follower for this term) results.addCallback( dropResult(self.state.requestVote, term=currentTerm, candidateId=candidateId, lastLogIndex=lastLogIndex, lastLogTerm=lastLogTerm)) results.addCallback(self.assertEquals, (currentTerm, False)) return results