commit 3dcc573e7aca39f6b12d74885d51b1d01b483f90 Author: Damian Johnson atagar@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})
tor-commits@lists.torproject.org