
commit 725e2f9deb1a22b5cfca0242ea79d5c0548a40e3 Author: Damian Johnson <atagar@torproject.org> Date: Sat Sep 8 10:26:48 2012 -0700 Unit tests for minimal vote and missing fields Unit tests for a couple important use cases and lots 'o fixes for the issues they uncovered. As mentioned earlier the 'validate' attribute took the wrong meaning in this parser so valid content errors and invalid content triggers stacktraces. --- stem/descriptor/networkstatus.py | 47 ++++++++++++++--- test/unit/descriptor/networkstatus/document.py | 65 +++++++++++++++++++++-- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py index 6f1f7b1..e55c06f 100644 --- a/stem/descriptor/networkstatus.py +++ b/stem/descriptor/networkstatus.py @@ -308,25 +308,49 @@ class NetworkStatusDocument(stem.descriptor.Descriptor): if validate and not self._validate_network_status_version(): raise ValueError("Invalid network-status-version: %s" % self.network_status_version) + vote = False if self.vote_status == "vote": vote = True elif self.vote_status == "consensus": vote = False elif validate: raise ValueError("Unrecognized vote-status") if vote: read_keyword_line("consensus-methods", True) - self.consensus_methods = [int(method) for method in self.consensus_methods.split(" ")] - self.published = _strptime(_read_keyword_line("published", content, validate, True), validate, True) + + if self.consensus_methods: + self.consensus_methods = [int(method) for method in self.consensus_methods.split(" ")] + else: + self.consensus_methods = [] + + line = _read_keyword_line("published", content, validate, True) + + if line: + self.published = _strptime(line, validate, True) else: read_keyword_line("consensus-method", True) if self.consensus_method != None: self.consensus_method = int(self.consensus_method) map(read_keyword_line, ["valid-after", "fresh-until", "valid-until"]) - self.valid_after = _strptime(self.valid_after, validate) - self.fresh_until = _strptime(self.fresh_until, validate) - self.valid_until = _strptime(self.valid_until, validate) + + if self.valid_after: + self.valid_after = _strptime(self.valid_after, validate) + elif validate: + raise ValueError("Missing the 'valid-after' field") + + if self.fresh_until: + self.fresh_until = _strptime(self.fresh_until, validate) + elif validate: + raise ValueError("Missing the 'fresh-until' field") + + if self.valid_until: + self.valid_until = _strptime(self.valid_until, validate) + elif validate: + raise ValueError("Missing the 'valid-until' field") + voting_delay = _read_keyword_line("voting-delay", content, validate) - self.vote_delay, self.dist_delay = [int(delay) for delay in voting_delay.split(" ")] + + if voting_delay: + self.vote_delay, self.dist_delay = [int(delay) for delay in voting_delay.split(" ")] client_versions = _read_keyword_line("client-versions", content, validate, True) if client_versions: @@ -334,7 +358,12 @@ class NetworkStatusDocument(stem.descriptor.Descriptor): server_versions = _read_keyword_line("server-versions", content, validate, True) if server_versions: self.server_versions = [stem.version.Version(version_string) for version_string in server_versions.split(",")] - self.known_flags = _read_keyword_line("known-flags", content, validate).split(" ") + + flags_content = _read_keyword_line("known-flags", content, validate) + + if flags_content: + self.known_flags = flags_content.split(" ") + read_keyword_line("params", True) if self.params: self.params = dict([(param.split("=")[0], int(param.split("=")[1])) for param in self.params.split(" ")]) @@ -346,7 +375,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor): self.directory_authorities.append(DirectoryAuthority(dirauth_data, vote, validate)) # footer section - if self.consensus_method >= 9 or vote and filter(lambda x: x >= 9, self.consensus_methods): + if self.consensus_method >= 9 or (vote and filter(lambda x: x >= 9, self.consensus_methods)): if _peek_keyword(content) == "directory-footer": content.readline() elif validate: @@ -364,7 +393,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor): remainder = content.read() if remainder: - self.unrecognized_lines = content.read().split("\n") + self.unrecognized_lines = remainder.split("\n") else: self.unrecognized_lines = [] diff --git a/test/unit/descriptor/networkstatus/document.py b/test/unit/descriptor/networkstatus/document.py index 027347b..8f770bb 100644 --- a/test/unit/descriptor/networkstatus/document.py +++ b/test/unit/descriptor/networkstatus/document.py @@ -10,6 +10,7 @@ from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_ NETWORK_STATUS_DOCUMENT_ATTR = { "network-status-version": "3", "vote-status": "consensus", + "consensus-methods": "9", "consensus-method": "9", "published": "2012-09-02 22:00:00", "valid-after": "2012-09-02 22:00:00", @@ -57,17 +58,20 @@ def get_network_status_document(attr = None, exclude = None, routers = None): if not field in attr: # Skip if it's not mandatory for this type of document. An exception is - # made for the consensus' consensus-method field since it influences - # validation, and is only missing for consensus-method lower than 2. + # made for the consensus' consensus-method and consensus-methods fields + # since it influences validation, and is only missing for + # consensus-method lower than 2. if field == "consensus-method" and is_consensus: pass - elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_vote)): + elif field == "consensus-methods" and is_vote: + pass + elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_votes)): continue if field in attr: - value = attr[keyword] - del attr[keyword] + value = attr[field] + del attr[field] elif field in NETWORK_STATUS_DOCUMENT_ATTR: value = NETWORK_STATUS_DOCUMENT_ATTR[field] @@ -82,7 +86,7 @@ def get_network_status_document(attr = None, exclude = None, routers = None): return "\n".join(header_content + remainder + routers + footer_content) class TestNetworkStatusDocument(unittest.TestCase): - def test_document_minimal(self): + def test_minimal_consensus(self): """ Parses a minimal network status document. """ @@ -114,4 +118,53 @@ class TestNetworkStatusDocument(unittest.TestCase): self.assertEqual(None, document.bandwidth_weights) self.assertEqual([sig], document.directory_signatures) self.assertEqual([], document.get_unrecognized_lines()) + + def test_minimal_vote(self): + """ + Parses a minimal network status document. + """ + + document = NetworkStatusDocument(get_network_status_document({"vote-status": "vote"})) + + expected_known_flags = [Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT, + Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING, + Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID] + + sig = DirectorySignature("directory-signature " + NETWORK_STATUS_DOCUMENT_ATTR["directory-signature"]) + + self.assertEqual((), document.routers) + self.assertEqual("3", document.network_status_version) + self.assertEqual("vote", document.vote_status) + self.assertEqual(None, document.consensus_method) + self.assertEqual([9], document.consensus_methods) + self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.published) + self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_after) + self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.fresh_until) + self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_until) + self.assertEqual(300, document.vote_delay) + self.assertEqual(300, document.dist_delay) + self.assertEqual([], document.client_versions) + self.assertEqual([], document.server_versions) + self.assertEqual(expected_known_flags, document.known_flags) + self.assertEqual(None, document.params) + self.assertEqual([], document.directory_authorities) + self.assertEqual({}, document.bandwidth_weights) + self.assertEqual([sig], document.directory_signatures) + self.assertEqual([], document.get_unrecognized_lines()) + + def test_missing_fields(self): + """ + Excludes mandatory fields from both a vote and consensus document. + """ + + for is_consensus in (True, False): + attr = {"vote-status": "consensus"} if is_consensus else {"vote-status": "vote"} + is_vote = not is_consensus + + for entries in (HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS): + for field, in_votes, in_consensus, is_mandatory in entries: + if is_mandatory and ((is_consensus and in_consensus) or (is_vote and in_votes)): + content = get_network_status_document(attr, exclude = (field,)) + self.assertRaises(ValueError, NetworkStatusDocument, content) + NetworkStatusDocument(content, False) # constructs without validation