[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