[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Minimal unit test for network status documents
commit cf475d58dfa4d42e982eca6307e2a61e7545147e
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Thu Sep 6 09:14:09 2012 -0700
Minimal unit test for network status documents
Adding a unit test for the minimal valid network status document (plus a
consensus-method field since that influences validation). This uncovered
some bugs with the NetworkStatusDocument class...
* The network_status_version field misdocumented as being an int (it was
actually a str). We need it to be a str for microdescriptors so simply
changed the pydoc.
* The consensus-method and bandwidth-weights are documented in the spec as
being optional fields. The parser errored with a stacktrace when
consensus-method was missing, and gave a validation error if there isn't a
bandwidth-weights.
* Inappropriate validation error if there was unrecognized content.
* The get_unrecognized_lines() method is documented as providing a list of
lines. The NetworkStatusDocument returned a string instead.
* Off-by-one error that caused consensus-method 9 documents to skip parsing
footers.
---
stem/descriptor/networkstatus.py | 28 ++++--
test/unit/descriptor/networkstatus.py | 173 ++++++++++++++++++++++++++------
2 files changed, 159 insertions(+), 42 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 168c768..6f1f7b1 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -198,11 +198,11 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
:var tuple routers: RouterStatusEntry contained in the document
- :var int network_status_version: **\*** document version
+ :var str network_status_version: **\*** document version
:var str vote_status: **\*** status of the vote (is either "vote" or "consensus")
+ :var int consensus_method: **~** consensus method used to generate a consensus
:var list consensus_methods: **^** A list of supported consensus generation methods (integers)
:var datetime published: **^** time when the document was published
- :var int consensus_method: **~** consensus method used to generate a consensus
:var datetime valid_after: **\*** time when the consensus becomes valid
:var datetime fresh_until: **\*** time until when the consensus is considered to be fresh
:var datetime valid_until: **\*** time until when the consensus is valid
@@ -318,7 +318,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.published = _strptime(_read_keyword_line("published", content, validate, True), validate, True)
else:
read_keyword_line("consensus-method", True)
- self.consensus_method = int(self.consensus_method)
+ 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)
@@ -345,7 +346,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:
@@ -353,17 +354,19 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
if not vote:
read_keyword_line("bandwidth-weights", True)
- if _bandwidth_weights_regex.match(self.bandwidth_weights):
+ if self.bandwidth_weights != None and _bandwidth_weights_regex.match(self.bandwidth_weights):
self.bandwidth_weights = dict([(weight.split("=")[0], int(weight.split("=")[1])) for weight in self.bandwidth_weights.split(" ")])
- elif validate:
- raise ValueError("Invalid bandwidth-weights line")
while _peek_keyword(content) == "directory-signature":
signature_data = _read_until_keywords("directory-signature", content, False, True)
self.directory_signatures.append(DirectorySignature("".join(signature_data)))
- self.unrecognized_lines = content.read()
- if validate and self.unrecognized_lines: raise ValueError("Unrecognized trailing data")
+ remainder = content.read()
+
+ if remainder:
+ self.unrecognized_lines = content.read().split("\n")
+ else:
+ self.unrecognized_lines = []
def _check_for_missing_and_disallowed_fields(self, is_consensus, header_entries, footer_entries):
"""
@@ -508,6 +511,13 @@ class DirectorySignature(stem.descriptor.Descriptor):
"""
return self.unrecognized_lines
+
+ def __cmp__(self, other):
+ if not isinstance(other, DirectorySignature):
+ return 1
+
+ # attributes are all derived from content, so we can simply use that to check
+ return str(self) > str(other)
class RouterStatusEntry(stem.descriptor.Descriptor):
"""
diff --git a/test/unit/descriptor/networkstatus.py b/test/unit/descriptor/networkstatus.py
index c7672e6..db1270e 100644
--- a/test/unit/descriptor/networkstatus.py
+++ b/test/unit/descriptor/networkstatus.py
@@ -5,15 +5,89 @@ Unit tests for stem.descriptor.networkstatus.
import datetime
import unittest
-from stem.descriptor.networkstatus import Flag, RouterStatusEntry, _decode_fingerprint
+from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS, Flag, NetworkStatusDocument, RouterStatusEntry, DirectorySignature, _decode_fingerprint
from stem.version import Version
from stem.exit_policy import MicrodescriptorExitPolicy
+NETWORK_STATUS_DOCUMENT_ATTR = {
+ "network-status-version": "3",
+ "vote-status": "consensus",
+ "consensus-method": "9",
+ "published": "2012-09-02 22:00:00",
+ "valid-after": "2012-09-02 22:00:00",
+ "fresh-until": "2012-09-02 22:00:00",
+ "valid-until": "2012-09-02 22:00:00",
+ "voting-delay": "300 300",
+ "known-flags": "Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid",
+ "directory-footer": "",
+ "directory-signature": "\n".join((
+ "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD",
+ "-----BEGIN SIGNATURE-----",
+ "e1XH33ITaUYzXu+dK04F2dZwR4PhcOQgIuK859KGpU77/6lRuggiX/INk/4FJanJ",
+ "ysCTE1K4xk4fH3N1Tzcv/x/gS4LUlIZz3yKfBnj+Xh3w12Enn9V1Gm1Vrhl+/YWH",
+ "eweONYRZTTvgsB+aYsCoBuoBBpbr4Swlu64+85F44o4=",
+ "-----END SIGNATURE-----")),
+}
+
ROUTER_STATUS_ENTRY_ATTR = (
("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
("s", "Fast Named Running Stable Valid"),
)
+def get_network_status_document(attr = None, exclude = None, routers = None):
+ """
+ Constructs a minimal network status document with the given attributes. This
+ places attributes in the proper order to be valid.
+
+ :param dict attr: keyword/value mappings to be included in the entry
+ :param list exclude: mandatory keywords to exclude from the entry
+ :param list routers: lines with router status entry content
+
+ :returns: str with customized router status entry content
+ """
+
+ descriptor_lines = []
+ if attr is None: attr = {}
+ if exclude is None: exclude = []
+ if routers is None: routers = []
+ attr = dict(attr) # shallow copy since we're destructive
+
+ is_vote = attr.get("vote-status") == "vote"
+ is_consensus = not is_vote
+
+ header_content, footer_content = [], []
+
+ for content, entries in ((header_content, HEADER_STATUS_DOCUMENT_FIELDS),
+ (footer_content, FOOTER_STATUS_DOCUMENT_FIELDS)):
+ for field, in_votes, in_consensus, is_mandatory in entries:
+ if field in exclude: continue
+
+ 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.
+
+ 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)):
+ continue
+
+ if field in attr:
+ value = attr[keyword]
+ del attr[keyword]
+ elif field in NETWORK_STATUS_DOCUMENT_ATTR:
+ value = NETWORK_STATUS_DOCUMENT_ATTR[field]
+
+ if value: value = " %s" % value
+ content.append(field + value)
+
+ remainder = []
+ for attr_keyword, attr_value in attr.items():
+ if attr_value: attr_value = " %s" % attr_value
+ remainder.append(attr_keyword + attr_value)
+
+ return "\n".join(header_content + remainder + routers + footer_content)
+
def get_router_status_entry(attr = None, exclude = None):
"""
Constructs a minimal router status entry with the given attributes.
@@ -67,7 +141,40 @@ class TestNetworkStatus(unittest.TestCase):
self.assertRaises(ValueError, _decode_fingerprint, arg, True)
self.assertEqual(None, _decode_fingerprint(arg, False))
- def test_rse_minimal(self):
+ def test_document_minimal(self):
+ """
+ Parses a minimal network status document.
+ """
+
+ document = NetworkStatusDocument(get_network_status_document())
+
+ 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("consensus", document.vote_status)
+ self.assertEqual(9, document.consensus_method)
+ self.assertEqual([], document.consensus_methods)
+ self.assertEqual(None, 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(None, document.bandwidth_weights)
+ self.assertEqual([sig], document.directory_signatures)
+ self.assertEqual([], document.get_unrecognized_lines())
+
+ def test_entry_minimal(self):
"""
Parses a minimal router status entry.
"""
@@ -93,21 +200,21 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEqual(None, entry.microdescriptor_hashes)
self.assertEqual([], entry.get_unrecognized_lines())
- def test_rse_missing_fields(self):
+ def test_entry_missing_fields(self):
"""
Parses a router status entry that's missing fields.
"""
content = get_router_status_entry(exclude = ('r', 's'))
- self._expect_invalid_rse_attr(content, "address")
+ self._expect_invalid_entry_attr(content, "address")
content = get_router_status_entry(exclude = ('r',))
- self._expect_invalid_rse_attr(content, "address")
+ self._expect_invalid_entry_attr(content, "address")
content = get_router_status_entry(exclude = ('s',))
- self._expect_invalid_rse_attr(content, "flags")
+ self._expect_invalid_entry_attr(content, "flags")
- def test_rse_unrecognized_lines(self):
+ def test_entry_unrecognized_lines(self):
"""
Parses a router status entry with new keywords.
"""
@@ -116,15 +223,15 @@ class TestNetworkStatus(unittest.TestCase):
entry = RouterStatusEntry(content, None)
self.assertEquals(['z New tor feature: sparkly unicorns!'], entry.get_unrecognized_lines())
- def test_rse_proceeding_line(self):
+ def test_entry_proceeding_line(self):
"""
Includes content prior to the 'r' line.
"""
content = 'z some stuff\n' + get_router_status_entry()
- self._expect_invalid_rse_attr(content, "_unrecognized_lines", ['z some stuff'])
+ self._expect_invalid_entry_attr(content, "_unrecognized_lines", ['z some stuff'])
- def test_rse_blank_lines(self):
+ def test_entry_blank_lines(self):
"""
Includes blank lines, which should be ignored.
"""
@@ -133,7 +240,7 @@ class TestNetworkStatus(unittest.TestCase):
entry = RouterStatusEntry(content, None)
self.assertEqual("Tor 0.2.2.35", entry.version_line)
- def test_rse_missing_r_field(self):
+ def test_entry_missing_r_field(self):
"""
Excludes fields from the 'r' line.
"""
@@ -155,9 +262,9 @@ class TestNetworkStatus(unittest.TestCase):
r_line = ' '.join(test_components)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, attr)
+ self._expect_invalid_entry_attr(content, attr)
- def test_rse_malformed_nickname(self):
+ def test_entry_malformed_nickname(self):
"""
Parses an 'r' line with a malformed nickname.
"""
@@ -184,9 +291,9 @@ class TestNetworkStatus(unittest.TestCase):
if value == "": value = None
- self._expect_invalid_rse_attr(content, "nickname", value)
+ self._expect_invalid_entry_attr(content, "nickname", value)
- def test_rse_malformed_fingerprint(self):
+ def test_entry_malformed_fingerprint(self):
"""
Parses an 'r' line with a malformed fingerprint.
"""
@@ -200,9 +307,9 @@ class TestNetworkStatus(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("p1aag7VwarGxqctS7/fS0y5FU+s", value)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, "fingerprint")
+ self._expect_invalid_entry_attr(content, "fingerprint")
- def test_rse_malformed_published_date(self):
+ def test_entry_malformed_published_date(self):
"""
Parses an 'r' line with a malformed published date.
"""
@@ -226,9 +333,9 @@ class TestNetworkStatus(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("2012-08-06 11:19:31", value)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, "published")
+ self._expect_invalid_entry_attr(content, "published")
- def test_rse_malformed_address(self):
+ def test_entry_malformed_address(self):
"""
Parses an 'r' line with a malformed address.
"""
@@ -244,9 +351,9 @@ class TestNetworkStatus(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("71.35.150.29", value)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, "address", value)
+ self._expect_invalid_entry_attr(content, "address", value)
- def test_rse_malformed_port(self):
+ def test_entry_malformed_port(self):
"""
Parses an 'r' line with a malformed ORPort or DirPort.
"""
@@ -276,9 +383,9 @@ class TestNetworkStatus(unittest.TestCase):
expected = int(value) if value.isdigit() else None
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, attr, expected)
+ self._expect_invalid_entry_attr(content, attr, expected)
- def test_rse_flags(self):
+ def test_entry_flags(self):
"""
Handles a variety of flag inputs.
"""
@@ -304,9 +411,9 @@ class TestNetworkStatus(unittest.TestCase):
for s_line, expected in test_values.items():
content = get_router_status_entry({'s': s_line})
- self._expect_invalid_rse_attr(content, "flags", expected)
+ self._expect_invalid_entry_attr(content, "flags", expected)
- def test_rse_versions(self):
+ def test_entry_versions(self):
"""
Handles a variety of version inputs.
"""
@@ -326,9 +433,9 @@ class TestNetworkStatus(unittest.TestCase):
# tries an invalid input
content = get_router_status_entry({'v': "Tor ugabuga"})
- self._expect_invalid_rse_attr(content, "version")
+ self._expect_invalid_entry_attr(content, "version")
- def test_rse_bandwidth(self):
+ def test_entry_bandwidth(self):
"""
Handles a variety of 'w' lines.
"""
@@ -363,9 +470,9 @@ class TestNetworkStatus(unittest.TestCase):
for w_line in test_values:
content = get_router_status_entry({'w': w_line})
- self._expect_invalid_rse_attr(content)
+ self._expect_invalid_entry_attr(content)
- def test_rse_exit_policy(self):
+ def test_entry_exit_policy(self):
"""
Handles a variety of 'p' lines.
"""
@@ -390,9 +497,9 @@ class TestNetworkStatus(unittest.TestCase):
for p_line in test_values:
content = get_router_status_entry({'p': p_line})
- self._expect_invalid_rse_attr(content, "exit_policy")
+ self._expect_invalid_entry_attr(content, "exit_policy")
- def test_rse_microdescriptor_hashes(self):
+ def test_entry_microdescriptor_hashes(self):
"""
Handles a variety of 'm' lines.
"""
@@ -417,7 +524,7 @@ class TestNetworkStatus(unittest.TestCase):
# try without a document
content = get_router_status_entry({'m': "8,9,10,11,12"})
- self._expect_invalid_rse_attr(content, "microdescriptor_hashes")
+ self._expect_invalid_entry_attr(content, "microdescriptor_hashes")
# tries some invalid inputs
test_values = (
@@ -430,7 +537,7 @@ class TestNetworkStatus(unittest.TestCase):
content = get_router_status_entry({'m': m_line})
self.assertRaises(ValueError, RouterStatusEntry, content, mock_document)
- def _expect_invalid_rse_attr(self, content, attr = None, expected_value = None):
+ def _expect_invalid_entry_attr(self, content, attr = None, expected_value = None):
"""
Asserts that construction will fail due to content having a malformed
attribute. If an attr is provided then we check that it matches an expected
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits