[tor-commits] [stem/master] BandwidthFile creation

atagar at torproject.org atagar at torproject.org
Mon Jan 21 01:52:11 UTC 2019


commit 3dcc573e7aca39f6b12d74885d51b1d01b483f90
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Jan 20 15:41:42 2019 -0800

    BandwidthFile creation
    
    Nothing fancy, just implementing Descriptor's create() and content() method.
---
 stem/descriptor/bandwidth_file.py      | 60 ++++++++++++++++++++-
 test/unit/descriptor/bandwidth_file.py | 98 ++++++++++++++++++++++++++++++++--
 2 files changed, 152 insertions(+), 6 deletions(-)

diff --git a/stem/descriptor/bandwidth_file.py b/stem/descriptor/bandwidth_file.py
index 97210d7b..0b2b211a 100644
--- a/stem/descriptor/bandwidth_file.py
+++ b/stem/descriptor/bandwidth_file.py
@@ -15,11 +15,14 @@ Parsing for Bandwidth Authority metrics as described in Tor's
 """
 
 import datetime
+import time
 
 import stem.util.str_tools
 
 from stem.descriptor import Descriptor
 
+HEADER_DIV = '====='
+
 
 # Converters header attributes to a given type. Malformed fields should be
 # ignored according to the spec.
@@ -93,7 +96,7 @@ def _parse_header(descriptor, entries):
     lines = lines[1:]
 
   for line in lines:
-    if line == '=====':
+    if line == HEADER_DIV:
       break
     elif line.startswith('node_id='):
       break  # version 1.0 measurement
@@ -155,8 +158,61 @@ class BandwidthFile(Descriptor):
 
   ATTRIBUTES.update(dict([(k, (None, _parse_header)) for k in HEADER_ATTR.keys()]))
 
+  @classmethod
+  def content(cls, attr = None, exclude = (), sign = False):
+    """
+    Creates descriptor content with the given attributes. This descriptor type
+    differs somewhat from others and treats our attr/exclude attributes as
+    follows...
+
+      * 'timestamp' is a reserved key for our mandatory header unix timestamp.
+
+      * 'content' is a reserved key for a list of our bandwidth measurements.
+
+      * All other keys are treated as header fields.
+
+    For example...
+
+    ::
+
+      BandwidthFile.content({
+        'timestamp': '12345',
+        'version': '1.2.0',
+        'content': [],
+      })
+    """
+
+    if sign:
+      raise NotImplementedError('Signing of %s not implemented' % cls.__name__)
+
+    header = dict(attr) if attr is not None else {}
+    timestamp = header.pop('timestamp', str(int(time.time())))
+    content = header.pop('content', [])
+    version = header.get('version', HEADER_DEFAULT.get('version'))
+
+    lines = []
+
+    if 'timestamp' not in exclude:
+      lines.append(timestamp)
+
+    if version == '1.0.0' and header:
+      raise ValueError('Headers require BandwidthFile version 1.1 or later')
+    elif version != '1.0.0':
+      for k, v in header.items():
+        lines.append('%s=%s' % (k, v))
+
+      lines.append(HEADER_DIV)
+
+    for measurement in content:
+      lines.append(measurement)  # TODO: replace when we have a measurement struct
+
+    return '\n'.join(lines)
+
   def __init__(self, raw_content, validate = False):
     super(BandwidthFile, self).__init__(raw_content, lazy_load = not validate)
 
+    self.content = []  # TODO: implement
+
     if validate:
-      pass  # TODO: implement eager load
+      _parse_timestamp(self, None)
+      _parse_header(self, None)
diff --git a/test/unit/descriptor/bandwidth_file.py b/test/unit/descriptor/bandwidth_file.py
index b8fdf01b..e1f8ffb4 100644
--- a/test/unit/descriptor/bandwidth_file.py
+++ b/test/unit/descriptor/bandwidth_file.py
@@ -6,9 +6,22 @@ import datetime
 import unittest
 
 import stem.descriptor
-import stem.descriptor.bandwidth_file
 
-import test.unit.descriptor
+from stem.descriptor.bandwidth_file import BandwidthFile
+from test.unit.descriptor import get_resource
+
+try:
+  # added in python 3.3
+  from unittest.mock import Mock, patch
+except ImportError:
+  from mock import Mock, patch
+
+EXPECTED_NEW_HEADER_CONTENT = """
+1410723598
+version=1.1.0
+new_header=neat stuff
+=====
+""".strip()
 
 
 class TestBandwidthFile(unittest.TestCase):
@@ -17,7 +30,7 @@ class TestBandwidthFile(unittest.TestCase):
     Parse version 1.0 formatted files.
     """
 
-    desc = list(stem.descriptor.parse_file(test.unit.descriptor.get_resource('bandwidth_file_v1.0'), 'badnwidth-file 1.0'))[0]
+    desc = list(stem.descriptor.parse_file(get_resource('bandwidth_file_v1.0'), 'badnwidth-file 1.0'))[0]
 
     self.assertEqual(datetime.datetime(2019, 1, 14, 17, 41, 29), desc.timestamp)
     self.assertEqual('1.0.0', desc.version)
@@ -41,7 +54,7 @@ class TestBandwidthFile(unittest.TestCase):
     Parse version 1.2 formatted files.
     """
 
-    desc = list(stem.descriptor.parse_file(test.unit.descriptor.get_resource('bandwidth_file_v1.2'), 'badnwidth-file 1.2'))[0]
+    desc = list(stem.descriptor.parse_file(get_resource('bandwidth_file_v1.2'), 'badnwidth-file 1.2'))[0]
 
     self.assertEqual(datetime.datetime(2019, 1, 14, 5, 34, 59), desc.timestamp)
     self.assertEqual('1.2.0', desc.version)
@@ -59,3 +72,80 @@ class TestBandwidthFile(unittest.TestCase):
     self.assertEqual(96, desc.eligible_percent)
     self.assertEqual(3908, desc.min_count)
     self.assertEqual(60, desc.min_percent)
+
+  @patch('time.time', Mock(return_value = 1410723598.276578))
+  def test_minimal_bandwidth_file(self):
+    """
+    Basic sanity check that we can parse a bandwidth file with minimal
+    attributes.
+    """
+
+    desc = BandwidthFile.create()
+
+    self.assertEqual('1410723598', str(desc))
+
+    self.assertEqual(datetime.datetime(2014, 9, 14, 19, 39, 58), desc.timestamp)
+    self.assertEqual('1.0.0', desc.version)
+
+    self.assertEqual(None, desc.software)
+    self.assertEqual(None, desc.software_version)
+
+    self.assertEqual(None, desc.earliest_bandwidth)
+    self.assertEqual(None, desc.latest_bandwidth)
+    self.assertEqual(None, desc.created_at)
+    self.assertEqual(None, desc.generated_at)
+
+    self.assertEqual(None, desc.consensus_size)
+    self.assertEqual(None, desc.eligible_count)
+    self.assertEqual(None, desc.eligible_percent)
+    self.assertEqual(None, desc.min_count)
+    self.assertEqual(None, desc.min_percent)
+
+    self.assertEqual({}, desc.header)
+
+  def test_content_example(self):
+    """
+    Exercise the example in our content method's pydoc.
+    """
+
+    content = BandwidthFile.content({
+      'timestamp': '12345',
+      'version': '1.2.0',
+      'content': [],
+    })
+
+    self.assertEqual('12345\nversion=1.2.0\n=====', content)
+
+  @patch('time.time', Mock(return_value = 1410723598.276578))
+  def test_new_header_attribute(self):
+    """
+    Include an unrecognized header field.
+    """
+
+    desc = BandwidthFile.create({'version': '1.1.0', 'new_header': 'neat stuff'})
+    self.assertEqual(EXPECTED_NEW_HEADER_CONTENT, str(desc))
+    self.assertEqual('1.1.0', desc.version)
+    self.assertEqual({'version': '1.1.0', 'new_header': 'neat stuff'}, desc.header)
+
+  def test_header_for_v1(self):
+    """
+    Document version 1.0 predates headers, and as such should be prohibited.
+    """
+
+    self.assertRaisesWith(ValueError, 'Headers require BandwidthFile version 1.1 or later', BandwidthFile.create, {'new_header': 'neat stuff'})
+
+  def test_invalid_timestamp(self):
+    """
+    Invalid timestamp values.
+    """
+
+    test_values = (
+      '',
+      'boo',
+      '123.4',
+      '-123',
+    )
+
+    for value in test_values:
+      expected_exc = "First line should be a unix timestamp, but was '%s'" % value
+      self.assertRaisesWith(ValueError, expected_exc, BandwidthFile.create, {'timestamp': value})





More information about the tor-commits mailing list