[tor-commits] [stem/master] Add a descriptor type_annotation method

atagar at torproject.org atagar at torproject.org
Mon Nov 12 01:16:16 UTC 2018


commit 1fa75186657a6b5f1f839314ed666ccf618abc7a
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Nov 11 17:11:38 2018 -0800

    Add a descriptor type_annotation method
    
    Interesting feature request from irl...
    
      https://trac.torproject.org/projects/tor/ticket/28397
    
    We can't knowledgeably provide a version number (those come from CollecTor,
    for instance to specify which bridge scrubbing specification metrics used). But
    we can certainly provide a valid annotation.
---
 docs/change_log.rst                                |  1 +
 stem/descriptor/__init__.py                        | 59 ++++++++++++++++++----
 stem/descriptor/extrainfo_descriptor.py            |  4 ++
 stem/descriptor/hidden_service_descriptor.py       |  2 +
 stem/descriptor/microdescriptor.py                 |  2 +
 stem/descriptor/networkstatus.py                   | 17 +++++++
 stem/descriptor/server_descriptor.py               |  4 ++
 stem/descriptor/tordnsel.py                        |  2 +
 test/unit/descriptor/extrainfo_descriptor.py       |  4 ++
 test/unit/descriptor/hidden_service_descriptor.py  |  1 +
 test/unit/descriptor/microdescriptor.py            |  2 +
 .../descriptor/networkstatus/bridge_document.py    |  1 +
 test/unit/descriptor/networkstatus/document_v2.py  |  1 +
 test/unit/descriptor/networkstatus/document_v3.py  |  4 ++
 .../descriptor/networkstatus/key_certificate.py    |  1 +
 test/unit/descriptor/server_descriptor.py          |  2 +
 test/unit/descriptor/tordnsel.py                   |  2 +
 17 files changed, 99 insertions(+), 10 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index 01f3f2a4..f8db0b52 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -51,6 +51,7 @@ The following are only available within Stem's `git repository
 
  * **Descriptors**
 
+  * Added :func:`~stem.descriptor.Descriptor.type_annotation` method (:trac:`28397`)
   * DescriptorDownloader crashed if **use_mirrors** is set (:trac:`28393`)
   * Don't download from Serge, a bridge authority that frequently timeout
 
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index a9860140..13c69f11 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -125,6 +125,24 @@ DocumentHandler = stem.util.enum.UppercaseEnum(
 )
 
 
+class TypeAnnotation(collections.namedtuple('TypeAnnotation', ['name', 'major_version', 'minor_version'])):
+  """
+  `Tor metrics type annotation
+  <https://metrics.torproject.org/collector.html#relay-descriptors>`_. The
+  string representation is the header annotation, for example "@type
+  server-descriptor 1.0".
+
+  .. versionadded:: 1.8.0
+
+  :var str name: name of the descriptor type
+  :var int major_version: major version number
+  :var int minor_version: minor version number
+  """
+
+  def __str__(self):
+    return '@type %s %s.%s' % (self.name, self.major_version, self.minor_version)
+
+
 class SigningKey(collections.namedtuple('SigningKey', ['private', 'public', 'public_digest'])):
   """
   Key used by relays to sign their server and extrainfo descriptors.
@@ -333,30 +351,30 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto
   # Parses descriptor files from metrics, yielding individual descriptors. This
   # throws a TypeError if the descriptor_type or version isn't recognized.
 
-  if descriptor_type == 'server-descriptor' and major_version == 1:
+  if descriptor_type == stem.descriptor.server_descriptor.RelayDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
     for desc in stem.descriptor.server_descriptor._parse_file(descriptor_file, is_bridge = False, validate = validate, **kwargs):
       yield desc
-  elif descriptor_type == 'bridge-server-descriptor' and major_version == 1:
+  elif descriptor_type == stem.descriptor.server_descriptor.BridgeDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
     for desc in stem.descriptor.server_descriptor._parse_file(descriptor_file, is_bridge = True, validate = validate, **kwargs):
       yield desc
-  elif descriptor_type == 'extra-info' and major_version == 1:
+  elif descriptor_type == stem.descriptor.extrainfo_descriptor.RelayExtraInfoDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
     for desc in stem.descriptor.extrainfo_descriptor._parse_file(descriptor_file, is_bridge = False, validate = validate, **kwargs):
       yield desc
-  elif descriptor_type == 'microdescriptor' and major_version == 1:
+  elif descriptor_type == stem.descriptor.microdescriptor.Microdescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
     for desc in stem.descriptor.microdescriptor._parse_file(descriptor_file, validate = validate, **kwargs):
       yield desc
-  elif descriptor_type == 'bridge-extra-info' and major_version == 1:
+  elif descriptor_type == stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
     # version 1.1 introduced a 'transport' field...
     # https://trac.torproject.org/6257
 
     for desc in stem.descriptor.extrainfo_descriptor._parse_file(descriptor_file, is_bridge = True, validate = validate, **kwargs):
       yield desc
-  elif descriptor_type == 'network-status-2' and major_version == 1:
+  elif descriptor_type == stem.descriptor.networkstatus.NetworkStatusDocumentV2.TYPE_ANNOTATION_NAME and major_version == 1:
     document_type = stem.descriptor.networkstatus.NetworkStatusDocumentV2
 
     for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, validate = validate, document_handler = document_handler, **kwargs):
       yield desc
-  elif descriptor_type == 'dir-key-certificate-3' and major_version == 1:
+  elif descriptor_type == stem.descriptor.networkstatus.KeyCertificate.TYPE_ANNOTATION_NAME and major_version == 1:
     for desc in stem.descriptor.networkstatus._parse_file_key_certs(descriptor_file, validate = validate, **kwargs):
       yield desc
   elif descriptor_type in ('network-status-consensus-3', 'network-status-vote-3') and major_version == 1:
@@ -369,17 +387,17 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto
 
     for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, is_microdescriptor = True, validate = validate, document_handler = document_handler, **kwargs):
       yield desc
-  elif descriptor_type == 'bridge-network-status' and major_version == 1:
+  elif descriptor_type == stem.descriptor.networkstatus.BridgeNetworkStatusDocument.TYPE_ANNOTATION_NAME and major_version == 1:
     document_type = stem.descriptor.networkstatus.BridgeNetworkStatusDocument
 
     for desc in stem.descriptor.networkstatus._parse_file(descriptor_file, document_type, validate = validate, document_handler = document_handler, **kwargs):
       yield desc
-  elif descriptor_type == 'tordnsel' and major_version == 1:
+  elif descriptor_type == stem.descriptor.tordnsel.TorDNSEL.TYPE_ANNOTATION_NAME and major_version == 1:
     document_type = stem.descriptor.tordnsel.TorDNSEL
 
     for desc in stem.descriptor.tordnsel._parse_file(descriptor_file, validate = validate, **kwargs):
       yield desc
-  elif descriptor_type == 'hidden-service-descriptor' and major_version == 1:
+  elif descriptor_type == stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor.TYPE_ANNOTATION_NAME and major_version == 1:
     document_type = stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor
 
     for desc in stem.descriptor.hidden_service_descriptor._parse_file(descriptor_file, validate = validate, **kwargs):
@@ -617,6 +635,7 @@ class Descriptor(object):
 
   ATTRIBUTES = {}  # mapping of 'attribute' => (default_value, parsing_function)
   PARSER_FOR_LINE = {}  # line keyword to its associated parsing function
+  TYPE_ANNOTATION_NAME = None
 
   def __init__(self, contents, lazy_load = False):
     self._path = None
@@ -675,6 +694,26 @@ class Descriptor(object):
 
     return cls(cls.content(attr, exclude, sign), validate = validate)
 
+  def type_annotation(self):
+    """
+    Provides the `Tor metrics annotation
+    <https://metrics.torproject.org/collector.html#relay-descriptors>`_ of this
+    descriptor type. For example, "@type server-descriptor 1.0" for server
+    descriptors.
+
+    Please note that the version number component is specific to CollecTor,
+    and for the moment hardcode as 1.0. This may change in the future.
+
+    .. versionadded:: 1.8.0
+
+    :returns: :class:`~stem.descriptor.TypeAnnotation` with our type information
+    """
+
+    if self.TYPE_ANNOTATION_NAME is not None:
+      return TypeAnnotation(self.TYPE_ANNOTATION_NAME, 1, 0)
+    else:
+      raise NotImplementedError('%s does not have a @type annotation' % type(self).__name__)
+
   def get_path(self):
     """
     Provides the absolute path that we loaded this descriptor from.
diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py
index 485b3063..8d8894d1 100644
--- a/stem/descriptor/extrainfo_descriptor.py
+++ b/stem/descriptor/extrainfo_descriptor.py
@@ -903,6 +903,8 @@ class RelayExtraInfoDescriptor(ExtraInfoDescriptor):
      Added the ed25519_certificate and ed25519_signature attributes.
   """
 
+  TYPE_ANNOTATION_NAME = 'extra-info'
+
   ATTRIBUTES = dict(ExtraInfoDescriptor.ATTRIBUTES, **{
     'ed25519_certificate': (None, _parse_identity_ed25519_line),
     'ed25519_signature': (None, _parse_router_sig_ed25519_line),
@@ -963,6 +965,8 @@ class BridgeExtraInfoDescriptor(ExtraInfoDescriptor):
      Added the ed25519_certificate_hash and router_digest_sha256 attributes.
   """
 
+  TYPE_ANNOTATION_NAME = 'bridge-extra-info'
+
   ATTRIBUTES = dict(ExtraInfoDescriptor.ATTRIBUTES, **{
     'ed25519_certificate_hash': (None, _parse_master_key_ed25519_line),
     'router_digest_sha256': (None, _parse_router_digest_sha256_line),
diff --git a/stem/descriptor/hidden_service_descriptor.py b/stem/descriptor/hidden_service_descriptor.py
index 83618dd8..a7cc0e3d 100644
--- a/stem/descriptor/hidden_service_descriptor.py
+++ b/stem/descriptor/hidden_service_descriptor.py
@@ -213,6 +213,8 @@ class HiddenServiceDescriptor(Descriptor):
      Added the **skip_crypto_validation** constructor argument.
   """
 
+  TYPE_ANNOTATION_NAME = 'hidden-service-descriptor'
+
   ATTRIBUTES = {
     'descriptor_id': (None, _parse_rendezvous_service_descriptor_line),
     'version': (None, _parse_version_line),
diff --git a/stem/descriptor/microdescriptor.py b/stem/descriptor/microdescriptor.py
index 731e8453..74a01071 100644
--- a/stem/descriptor/microdescriptor.py
+++ b/stem/descriptor/microdescriptor.py
@@ -233,6 +233,8 @@ class Microdescriptor(Descriptor):
      Added the protocols attribute.
   """
 
+  TYPE_ANNOTATION_NAME = 'microdescriptor'
+
   ATTRIBUTES = {
     'onion_key': (None, _parse_onion_key_line),
     'ntor_onion_key': (None, _parse_ntor_onion_key_line),
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 57098e81..63483c94 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -65,6 +65,7 @@ import stem.version
 from stem.descriptor import (
   PGP_BLOCK_END,
   Descriptor,
+  TypeAnnotation,
   DocumentHandler,
   _descriptor_content,
   _descriptor_components,
@@ -442,6 +443,8 @@ class NetworkStatusDocumentV2(NetworkStatusDocument):
   a default value, others are left as **None** if undefined
   """
 
+  TYPE_ANNOTATION_NAME = 'network-status-2'
+
   ATTRIBUTES = {
     'version': (None, _parse_network_status_version_line),
     'hostname': (None, _parse_dir_source_line),
@@ -1088,6 +1091,16 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
     self.routers = dict((desc.fingerprint, desc) for desc in router_iter)
     self._footer(document_file, validate)
 
+  def type_annotation(self):
+    if not self.is_microdescriptor:
+      return TypeAnnotation('network-status-consensus-3' if not self.is_vote else 'network-status-vote-3', 1, 0)
+    else:
+      # Directory authorities do not issue a 'microdescriptor consensus' vote,
+      # so unlike the above there isn't a 'network-status-microdesc-vote-3'
+      # counterpart here.
+
+      return TypeAnnotation('network-status-microdesc-consensus-3', 1, 0)
+
   def validate_signatures(self, key_certs):
     """
     Validates we're properly signed by the signing certificates.
@@ -1613,6 +1626,8 @@ class KeyCertificate(Descriptor):
   **\*** mandatory attribute
   """
 
+  TYPE_ANNOTATION_NAME = 'dir-key-certificate-3'
+
   ATTRIBUTES = {
     'version': (None, _parse_dir_key_certificate_version_line),
     'address': (None, _parse_dir_address_line),
@@ -1766,6 +1781,8 @@ class BridgeNetworkStatusDocument(NetworkStatusDocument):
   :var datetime published: time when the document was published
   """
 
+  TYPE_ANNOTATION_NAME = 'bridge-network-status'
+
   def __init__(self, raw_content, validate = False):
     super(BridgeNetworkStatusDocument, self).__init__(raw_content)
 
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index d212459e..0d12f875 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -793,6 +793,8 @@ class RelayDescriptor(ServerDescriptor):
      Added the **skip_crypto_validation** constructor argument.
   """
 
+  TYPE_ANNOTATION_NAME = 'server-descriptor'
+
   ATTRIBUTES = dict(ServerDescriptor.ATTRIBUTES, **{
     'certificate': (None, _parse_identity_ed25519_line),
     'ed25519_certificate': (None, _parse_identity_ed25519_line),
@@ -997,6 +999,8 @@ class BridgeDescriptor(ServerDescriptor):
      descriptors).
   """
 
+  TYPE_ANNOTATION_NAME = 'bridge-server-descriptor'
+
   ATTRIBUTES = dict(ServerDescriptor.ATTRIBUTES, **{
     'ed25519_certificate_hash': (None, _parse_master_key_ed25519_for_hash_line),
     'router_digest_sha256': (None, _parse_router_digest_sha256_line),
diff --git a/stem/descriptor/tordnsel.py b/stem/descriptor/tordnsel.py
index b573b79c..c4aba296 100644
--- a/stem/descriptor/tordnsel.py
+++ b/stem/descriptor/tordnsel.py
@@ -60,6 +60,8 @@ class TorDNSEL(Descriptor):
   a default value, others are left as **None** if undefined
   """
 
+  TYPE_ANNOTATION_NAME = 'tordnsel'
+
   def __init__(self, raw_contents, validate):
     super(TorDNSEL, self).__init__(raw_contents)
     raw_contents = stem.util.str_tools._to_unicode(raw_contents)
diff --git a/test/unit/descriptor/extrainfo_descriptor.py b/test/unit/descriptor/extrainfo_descriptor.py
index d649040a..f3252d4e 100644
--- a/test/unit/descriptor/extrainfo_descriptor.py
+++ b/test/unit/descriptor/extrainfo_descriptor.py
@@ -72,6 +72,8 @@ k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
     dir_write_values_start = [0, 0, 0, 227328, 349184, 382976, 738304]
     self.assertEqual(dir_write_values_start, desc.dir_write_history_values[:7])
 
+    self.assertEqual('@type extra-info 1.0', str(desc.type_annotation()))
+
   def test_metrics_bridge_descriptor(self):
     """
     Parses and checks our results against an extrainfo bridge descriptor from
@@ -133,6 +135,8 @@ k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
     self.assertEqual({}, desc.dir_v2_responses_unknown)
     self.assertEqual({}, desc.dir_v2_responses_unknown)
 
+    self.assertEqual('@type bridge-extra-info 1.0', str(desc.type_annotation()))
+
   @test.require.cryptography
   def test_descriptor_signing(self):
     RelayExtraInfoDescriptor.create(sign = True)
diff --git a/test/unit/descriptor/hidden_service_descriptor.py b/test/unit/descriptor/hidden_service_descriptor.py
index 23067c62..437366a2 100644
--- a/test/unit/descriptor/hidden_service_descriptor.py
+++ b/test/unit/descriptor/hidden_service_descriptor.py
@@ -424,6 +424,7 @@ class TestHiddenServiceDescriptor(unittest.TestCase):
     self.assertEqual([], desc.introduction_points_auth)
     self.assertEqual(b'', desc.introduction_points_content)
     self.assertEqual([], desc.introduction_points())
+    self.assertEqual('@type hidden-service-descriptor 1.0', str(desc.type_annotation()))
 
   def test_unrecognized_line(self):
     """
diff --git a/test/unit/descriptor/microdescriptor.py b/test/unit/descriptor/microdescriptor.py
index 5f245619..bb4b91a2 100644
--- a/test/unit/descriptor/microdescriptor.py
+++ b/test/unit/descriptor/microdescriptor.py
@@ -74,6 +74,8 @@ class TestMicrodescriptor(unittest.TestCase):
       self.assertEqual({b'@last-listed': b'2013-02-24 00:18:36'}, router.get_annotations())
       self.assertEqual([b'@last-listed 2013-02-24 00:18:36'], router.get_annotation_lines())
 
+      self.assertEqual('@type microdescriptor 1.0', str(router.type_annotation()))
+
   def test_minimal_microdescriptor(self):
     """
     Basic sanity check that we can parse a microdescriptor with minimal
diff --git a/test/unit/descriptor/networkstatus/bridge_document.py b/test/unit/descriptor/networkstatus/bridge_document.py
index d027c94d..97e3e178 100644
--- a/test/unit/descriptor/networkstatus/bridge_document.py
+++ b/test/unit/descriptor/networkstatus/bridge_document.py
@@ -51,6 +51,7 @@ class TestBridgeNetworkStatusDocument(unittest.TestCase):
     self.assertEqual(datetime.datetime(2012, 6, 1, 4, 7, 4), document.published)
     self.assertEqual({}, document.routers)
     self.assertEqual([], document.get_unrecognized_lines())
+    self.assertEqual('@type bridge-network-status 1.0', str(document.type_annotation()))
 
   def test_document(self):
     """
diff --git a/test/unit/descriptor/networkstatus/document_v2.py b/test/unit/descriptor/networkstatus/document_v2.py
index a02ea7a5..7dcc235b 100644
--- a/test/unit/descriptor/networkstatus/document_v2.py
+++ b/test/unit/descriptor/networkstatus/document_v2.py
@@ -56,6 +56,7 @@ TpQQk3nNQF8z6UIvdlvP+DnJV4izWVkQEZgUZgIVM0E=
       self.assertEqual([], document.get_unrecognized_lines())
 
       self.assertEqual(3, len(document.routers))
+      self.assertEqual('@type network-status-2 1.0', str(document.type_annotation()))
 
       router1 = document.routers['719BE45DE224B607C53707D0E2143E2D423E74CF']
       self.assertEqual('moria2', router1.nickname)
diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py
index 18d9088a..e4258dab 100644
--- a/test/unit/descriptor/networkstatus/document_v3.py
+++ b/test/unit/descriptor/networkstatus/document_v3.py
@@ -121,6 +121,7 @@ ci356fosgLiM1sVqCUkNdA==
       self.assertEqual([], document.consensus_methods)
       self.assertEqual(None, document.published)
       self.assertEqual([], document.get_unrecognized_lines())
+      self.assertEqual('@type network-status-consensus-3 1.0', str(document.type_annotation()))
 
       router = document.routers['348225F83C854796B2DD6364E65CB189B33BD696']
       self.assertEqual('test002r', router.nickname)
@@ -254,6 +255,7 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
       self.assertEqual('178.218.213.229', router.address)
       self.assertEqual(80, router.or_port)
       self.assertEqual(None, router.dir_port)
+      self.assertEqual('@type network-status-vote-3 1.0', str(document.type_annotation()))
 
       authority = document.directory_authorities[0]
       self.assertEqual(1, len(document.directory_authorities))
@@ -1143,6 +1145,8 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
     self.assertTrue(entry1 in document.routers.values())
     self.assertTrue(entry2 in document.routers.values())
 
+    self.assertEqual('@type network-status-microdesc-consensus-3 1.0', str(document.type_annotation()))
+
     # try with an invalid RouterStatusEntry
 
     entry3 = RouterStatusEntryMicroV3(RouterStatusEntryMicroV3.content({'r': 'ugabuga'}), False)
diff --git a/test/unit/descriptor/networkstatus/key_certificate.py b/test/unit/descriptor/networkstatus/key_certificate.py
index 0da8da11..985610fb 100644
--- a/test/unit/descriptor/networkstatus/key_certificate.py
+++ b/test/unit/descriptor/networkstatus/key_certificate.py
@@ -89,6 +89,7 @@ PPc3r7zKlL/jEGHwz+C7kE88HIvkVnKLLn//40b6HxitHSOCkZ1vtp8YyXae6xnU
       self.assertEqual(expected_signing_key, cert.signing_key)
       self.assertEqual(expected_crosscert, cert.crosscert)
       self.assertEqual(expected_key_cert, cert.certification)
+      self.assertEqual('@type dir-key-certificate-3 1.0', str(cert.type_annotation()))
       self.assertEqual([], cert.get_unrecognized_lines())
 
   def test_metrics_certificate(self):
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index 6e6d27d4..ec1af553 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -155,6 +155,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     self.assertEqual([], desc.get_unrecognized_lines())
     self.assertEqual('2C7B27BEAB04B4E2459D89CA6D5CD1CC5F95A689', desc.digest())
 
+    self.assertEqual('@type server-descriptor 1.0', str(desc.type_annotation()))
     self.assertEqual(['2'], desc.hidden_service_dir)  # obsolete field
 
   def test_metrics_descriptor_multiple(self):
@@ -436,6 +437,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     self.assertFalse(hasattr(desc, 'ed25519_certificate'))
     self.assertEqual('lgIuiAJCoXPRwWoHgG4ZAoKtmrv47aPr4AsbmESj8AA', desc.ed25519_certificate_hash)
     self.assertEqual('OB/fqLD8lYmjti09R+xXH/D4S2qlizxdZqtudnsunxE', desc.router_digest_sha256)
+    self.assertEqual('@type bridge-server-descriptor 1.0', str(desc.type_annotation()))
     self.assertEqual([], desc.get_unrecognized_lines())
 
   def test_cr_in_contact_line(self):
diff --git a/test/unit/descriptor/tordnsel.py b/test/unit/descriptor/tordnsel.py
index f6cf07ff..fbc17442 100644
--- a/test/unit/descriptor/tordnsel.py
+++ b/test/unit/descriptor/tordnsel.py
@@ -86,3 +86,5 @@ class TestTorDNSELDescriptor(unittest.TestCase):
     self.assertTrue(is_valid_fingerprint(desc.fingerprint))
     self.assertEqual('030B22437D99B2DB2908B747B6962EAD13AB4038', desc.fingerprint)
     self.assertEqual(0, len(desc.exit_addresses))
+
+    self.assertEqual('@type tordnsel 1.0', str(desc.type_annotation()))



More information about the tor-commits mailing list