commit a349a01fece5534195a6619d72df4944065001b8 Author: Damian Johnson atagar@torproject.org Date: Sat Nov 17 18:49:23 2012 -0800
Support for CIRC events
Implementation and testing for CIRC events. This work also concerns the 'GETINFO circuit-status' method, which is defined as providing the same output as CIRC events. This is part of the reason why I put the enums for the event attributes in 'stem.control'. --- docs/api/response.rst | 1 + run_tests.py | 2 + stem/control.py | 227 +++++++++++++++++++++++++++++++++++++++ stem/response/events.py | 92 +++++++++++++++- stem/util/str_tools.py | 36 ++++++ stem/util/tor_tools.py | 18 +++ test/unit/__init__.py | 1 + test/unit/control/__init__.py | 6 + test/unit/control/controller.py | 50 +++++++++ test/unit/response/events.py | 124 +++++++++++++++++++++- test/unit/util/str_tools.py | 31 ++++++ 11 files changed, 582 insertions(+), 6 deletions(-)
diff --git a/docs/api/response.rst b/docs/api/response.rst index fd6e2cc..043cae4 100644 --- a/docs/api/response.rst +++ b/docs/api/response.rst @@ -18,4 +18,5 @@ Events .. autoclass:: stem.response.events.Event .. autoclass:: stem.response.events.LogEvent .. autoclass:: stem.response.events.BandwidthEvent +.. autoclass:: stem.response.events.CircuitEvent
diff --git a/run_tests.py b/run_tests.py index f490da7..a9aebbe 100755 --- a/run_tests.py +++ b/run_tests.py @@ -16,6 +16,7 @@ import test.output import test.runner import test.check_whitespace import test.unit.connection.authentication +import test.unit.control.controller import test.unit.descriptor.export import test.unit.descriptor.reader import test.unit.descriptor.server_descriptor @@ -142,6 +143,7 @@ UNIT_TESTS = ( test.unit.response.protocolinfo.TestProtocolInfoResponse, test.unit.response.authchallenge.TestAuthChallengeResponse, test.unit.connection.authentication.TestAuthenticate, + test.unit.control.controller.TestControl, )
INTEG_TESTS = ( diff --git a/stem/control.py b/stem/control.py index 1c1139f..083082e 100644 --- a/stem/control.py +++ b/stem/control.py @@ -78,7 +78,110 @@ providing its own for interacting at a higher level. **WARN** :class:`stem.response.events.LogEvent` **ERR** :class:`stem.response.events.LogEvent` **BW** :class:`stem.response.events.BandwidthEvent` + **CIRC** :class:`stem.response.events.CircuitEvent` =========== =========== + +.. data:: CircStatus (enum) + + Statuses that a circuit can be in. Tor may provide statuses not in this enum. + + ============ =========== + CircStatus Description + ============ =========== + **LAUNCHED** new circuit was created + **BUILT** circuit finished being created and can accept traffic + **EXTENDED** circuit has been extended by a hop + **FAILED** circuit construction failed + **CLOSED** circuit has been closed + ============ =========== + +.. data:: CircBuildFlag (enum) + + Attributes about how a circuit is built. These were introduced in tor version + 0.2.3.11. Tor may provide flags not in this enum. + + ================= =========== + CircBuildFlag Description + ================= =========== + **ONEHOP_TUNNEL** single hop circuit to fetch directory information + **IS_INTERNAL** circuit that won't be used for client traffic + **NEED_CAPACITY** circuit only includes high capacity relays + **NEED_UPTIME** circuit only includes relays with a high uptime + ================= =========== + +.. data:: CircPurpose (enum) + + Description of what a circuit is intended for. These were introduced in tor + version 0.2.1.6. Tor may provide purposes not in this enum. + + ==================== =========== + CircPurpose Description + ==================== =========== + **GENERAL** client traffic or fetching directory information + **HS_CLIENT_INTRO** client side introduction point for a hidden service circuit + **HS_CLIENT_REND** client side hidden service rendezvous circuit + **HS_SERVICE_INTRO** server side introduction point for a hidden service circuit + **HS_SERVICE_REND** server side hidden service rendezvous circuit + **TESTING** testing to see if we're reachable, so we can be used as a relay + **CONTROLLER** circuit that was built by a controller + ==================== =========== + +.. data:: CircClosureReason (enum) + + Reason that a circuit is being closed or failed to be established. Tor may + provide purposes not in this enum. + + ========================= =========== + CircClosureReason Description + ========================= =========== + **NONE** no reason given + **TORPROTOCOL** violation in the tor protocol + **INTERNAL** internal error + **REQUESTED** requested by the client via a TRUNCATE command + **HIBERNATING** relay is presently hibernating + **RESOURCELIMIT** relay is out of memory, sockets, or circuit IDs + **CONNECTFAILED** unable to contact the relay + **OR_IDENTITY** relay had the wrong OR identification + **OR_CONN_CLOSED** connection failed after being established + **FINISHED** circuit has expired (see tor's MaxCircuitDirtiness config option) + **TIMEOUT** circuit construction timed out + **DESTROYED** circuit unexpectedly closed + **NOPATH** not enough relays to make a circuit + **NOSUCHSERVICE** requested hidden service does not exist + **MEASUREMENT_EXPIRED** unknown (https://trac.torproject.org/7506) + ========================= =========== + +.. data:: HiddenServiceState (enum) + + State that a hidden service circuit can have. These were introduced in tor + version 0.2.3.11. Tor may provide states not in this enum. + + Enumerations fall into four groups based on their prefix... + + ======= =========== + Prefix Description + ======= =========== + HSCI_* client-side introduction-point + HSCR_* client-side rendezvous-point + HSSI_* service-side introduction-point + HSSR_* service-side rendezvous-point + ======= =========== + + ============================= =========== + HiddenServiceState Description + ============================= =========== + **HSCI_CONNECTING** connecting to the introductory point + **HSCI_INTRO_SENT** sent INTRODUCE1 and awaiting a reply + **HSCI_DONE** received a reply, circuit is closing + **HSCR_CONNECTING** connecting to the introductory point + **HSCR_ESTABLISHED_IDLE** rendezvous-point established, awaiting an introduction + **HSCR_ESTABLISHED_WAITING** introduction received, awaiting a rend + **HSCR_JOINED** connected to the hidden service + **HSSI_CONNECTING** connecting to the introductory point + **HSSI_ESTABLISHED** established introductory point + **HSSR_CONNECTING** connecting to the introductory point + **HSSR_JOINED** connected to the rendezvous-point + ============================= =========== """
from __future__ import with_statement @@ -129,6 +232,63 @@ EventType = stem.util.enum.UppercaseEnum( "CIRC_MINOR", )
+CircStatus = stem.util.enum.UppercaseEnum( + "LAUNCHED", + "BUILT", + "EXTENDED", + "FAILED", + "CLOSED", +) + +CircBuildFlag = stem.util.enum.UppercaseEnum( + "ONEHOP_TUNNEL", + "IS_INTERNAL", + "NEED_CAPACITY", + "NEED_UPTIME", +) + +CircPurpose = stem.util.enum.UppercaseEnum( + "GENERAL", + "HS_CLIENT_INTRO", + "HS_CLIENT_REND", + "HS_SERVICE_INTRO", + "HS_SERVICE_REND", + "TESTING", + "CONTROLLER", +) + +CircClosureReason = stem.util.enum.UppercaseEnum( + "NONE", + "TORPROTOCOL", + "INTERNAL", + "REQUESTED", + "HIBERNATING", + "RESOURCELIMIT", + "CONNECTFAILED", + "OR_IDENTITY", + "OR_CONN_CLOSED", + "FINISHED", + "TIMEOUT", + "DESTROYED", + "NOPATH", + "NOSUCHSERVICE", + "MEASUREMENT_EXPIRED", +) + +HiddenServiceState = stem.util.enum.UppercaseEnum( + "HSCI_CONNECTING", + "HSCI_INTRO_SENT", + "HSCI_DONE", + "HSCR_CONNECTING", + "HSCR_ESTABLISHED_IDLE", + "HSCR_ESTABLISHED_WAITING", + "HSCR_JOINED", + "HSSI_CONNECTING", + "HSSI_ESTABLISHED", + "HSSR_CONNECTING", + "HSSR_JOINED", +) + # Constant to indicate an undefined argument default. Usually we'd use None for # this, but users will commonly provide None as the argument so need something # else fairly unique... @@ -521,6 +681,9 @@ class Controller(BaseController): BaseController and provides a more user friendly API for library users. """
+ # TODO: We need a set_up() (and maybe tear_down()?) method, so we can + # reattach listeners and set VERBOSE_NAMES. + def from_port(control_addr = "127.0.0.1", control_port = 9051): """ Constructs a :class:`~stem.socket.ControlPort` based Controller. @@ -1462,6 +1625,70 @@ class Controller(BaseController): for listener in event_listeners: listener(event_message)
+def _parse_circ_path(path): + """ + Parses a circuit path as a list of **(fingerprint, nickname)** tuples. Tor + circuit paths are defined as being of the form... + + :: + + Path = LongName *("," LongName) + LongName = Fingerprint [ ( "=" / "~" ) Nickname ] + + example: + $999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz + + ... *unless* this is prior to tor version 0.2.2.1 with the VERBOSE_NAMES + feature turned off (or before version 0.1.2.2 where the feature was + introduced). In that case either the fingerprint or nickname in the tuple + will be **None**, depending on which is missing. + + :: + + Path = ServerID *("," ServerID) + ServerID = Nickname / Fingerprint + + example: + $E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14 + + :param str path: circuit path to be parsed + + :returns: list of **(fingerprint, nickname)** tuples, fingerprints do not have a proceeding '$' + + :raises: :class:`stem.ProtocolError` if the path is malformed + """ + + if not path: return [] + + circ_path = [] + + for path_component in path.split(','): + if '=' in path_component: + # common case + fingerprint, nickname = path_component.split('=') + elif '~' in path_component: + # this is allowed for by the spec, but I've never seen it used + fingerprint, nickname = path_component.split('~') + elif path_component[0] == '$': + # old style, fingerprint only + fingerprint, nickname = path_component, None + else: + # old style, nickname only + fingerprint, nickname = None, path_component + + if fingerprint != None: + if not stem.util.tor_tools.is_valid_fingerprint(fingerprint, True): + raise stem.ProtocolError("Fingerprint in the circuit path is malformed (%s): %s" % (fingerprint, path)) + + fingerprint = fingerprint[1:] # strip off the leading '$' + + if nickname != None and not stem.util.tor_tools.is_valid_nickname(nickname): + raise stem.ProtocolError("Nickname in the circuit path is malformed (%s): %s" % (fingerprint, path)) + + circ_path.append((fingerprint, nickname)) + + return circ_path + def _case_insensitive_lookup(entries, key, default = UNDEFINED): """ Makes a case insensitive lookup within a list or dictionary, providing the diff --git a/stem/response/events.py b/stem/response/events.py index 93c3191..4c8d8bd 100644 --- a/stem/response/events.py +++ b/stem/response/events.py @@ -1,7 +1,9 @@ import re
+import stem.control import stem.response -import stem.socket + +from stem.util import log, str_tools, tor_tools
# Matches keyword=value arguments. This can't be a simple "(.*)=(.*)" pattern # because some positional arguments, like circuit paths, can have an equal @@ -28,7 +30,7 @@ class Event(stem.response.ControlMessage): fields = str(self).split()
if not fields: - raise stem.socket.ProtocolError("Received a blank tor event. Events must at the very least have a type.") + raise stem.ProtocolError("Received a blank tor event. Events must at the very least have a type.")
self.type = fields.pop(0) self.arrived_at = arrived_at @@ -75,6 +77,85 @@ class Event(stem.response.ControlMessage): def _parse(self): pass
+class CircuitEvent(Event): + """ + Event that indicates that a circuit has changed. + + The fingerprint or nickname values in our path may be **None** if the + VERBOSE_NAMES feature is unavailable. The option was first introduced in tor + version 0.1.2.2. + + :var str id: circuit identifier + :var stem.control.CircStatus status: reported status for the circuit + :var tuple path: relays involved in the circuit, these are + **(fingerprint, nickname)** tuples + :var tuple build_flags: :data:`~stem.control.CircBuildFlag` attributes + governing how the circuit is built + :var stem.control.CircPurpose purpose: purpose that the circuit is intended for + :var stem.control.HiddenServiceState hs_state: status if this is a hidden service circuit + :var str rend_query: circuit's rendezvous-point if this is hidden service related + :var datetime created: time when the circuit was created or cannibalized + :var stem.control.CircClosureReason reason: reason for the circuit to be closed + :var stem.control.CircClosureReason remote_reason: remote side's reason for the circuit to be closed + """ + + _POSITIONAL_ARGS = ("id", "status", "path") + _KEYWORD_ARGS = { + "BUILD_FLAGS": "build_flags", + "PURPOSE": "purpose", + "HS_STATE": "hs_state", + "REND_QUERY": "rend_query", + "TIME_CREATED": "created", + "REASON": "reason", + "REMOTE_REASON": "remote_reason", + } + + def _parse(self): + self.path = tuple(stem.control._parse_circ_path(self.path)) + + if self.build_flags != None: + self.build_flags = tuple(self.build_flags.split(',')) + + if self.created != None: + try: + self.created = str_tools.parse_iso_timestamp(self.created) + except ValueError, exc: + raise stem.ProtocolError("Unable to parse create date (%s): %s" % (exc, self)) + + if self.id != None and not tor_tools.is_valid_circuit_id(self.id): + raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) + + # log if we have an unrecognized status, build flag, purpose, hidden + # service state, or closure reason + + unrecognized_msg = "CIRC event had an unrecognised %%s (%%s). Maybe a new addition to the control protocol? Full Event: '%s'" % self + + if self.status and (not self.status in stem.control.CircStatus): + log_id = "event.circ.unknown_status.%s" % self.status + log.log_once(log_id, log.INFO, unrecognized_msg % ('status', self.status)) + + if self.build_flags: + for flag in self.build_flags: + if not flag in stem.control.CircBuildFlag: + log_id = "event.circ.unknown_build_flag.%s" % flag + log.log_once(log_id, log.INFO, unrecognized_msg % ('build flag', flag)) + + if self.purpose and (not self.purpose in stem.control.CircPurpose): + log_id = "event.circ.unknown_purpose.%s" % self.purpose + log.log_once(log_id, log.INFO, unrecognized_msg % ('purpose', self.purpose)) + + if self.hs_state and (not self.hs_state in stem.control.HiddenServiceState): + log_id = "event.circ.unknown_hs_state.%s" % self.hs_state + log.log_once(log_id, log.INFO, unrecognized_msg % ('hidden service state', self.hs_state)) + + if self.reason and (not self.reason in stem.control.CircClosureReason): + log_id = "event.circ.unknown_reason.%s" % self.reason + log.log_once(log_id, log.INFO, unrecognized_msg % ('reason', self.reason)) + + if self.remote_reason and (not self.remote_reason in stem.control.CircClosureReason): + log_id = "event.circ.unknown_remote_reason.%s" % self.remote_reason + log.log_once(log_id, log.INFO, unrecognized_msg % ('remote reason', self.remote_reason)) + class BandwidthEvent(Event): """ Event emitted every second with the bytes sent and received by tor. @@ -87,11 +168,11 @@ class BandwidthEvent(Event):
def _parse(self): if not self.read: - raise stem.socket.ProtocolError("BW event is missing its read value") + raise stem.ProtocolError("BW event is missing its read value") elif not self.written: - raise stem.socket.ProtocolError("BW event is missing its written value") + raise stem.ProtocolError("BW event is missing its written value") elif not self.read.isdigit() or not self.written.isdigit(): - raise stem.socket.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self) + raise stem.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self)
self.read = long(self.read) self.written = long(self.written) @@ -114,6 +195,7 @@ class LogEvent(Event): self.message = str(self)[len(self.runlevel) + 1:].rstrip("\nOK")
EVENT_TYPE_TO_CLASS = { + "CIRC": CircuitEvent, "BW": BandwidthEvent, "DEBUG": LogEvent, "INFO": LogEvent, diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py index f444760..6c36cd0 100644 --- a/stem/util/str_tools.py +++ b/stem/util/str_tools.py @@ -11,8 +11,12 @@ Toolkit for various string activity. get_time_labels - human readable labels for each time unit get_short_time_label - condensed time label output parse_short_time_label - seconds represented by a short time label + + parse_iso_timestamp - parses an ISO timestamp as a datetime value """
+import datetime + # label conversion tuples of the form... # (bits / bytes / seconds, short label, long label) SIZE_UNITS_BITS = ( @@ -235,6 +239,38 @@ def parse_short_time_label(label): except ValueError: raise ValueError("Non-numeric value in time entry: %s" % label)
+def parse_iso_timestamp(entry): + """ + Parses the ISO 8601 standard that provides for timestamps like... + + :: + + 2012-11-08T16:48:41.420251 + + :param str entry: timestamp to be parsed + + :returns: datetime for the time represented by the timestamp + + :raises: ValueError if the timestamp is malformed + """ + + if not isinstance(entry, str): + raise ValueError("parse_iso_timestamp() input must be a str, got a %s" % type(entry)) + + # based after suggestions from... + # http://stackoverflow.com/questions/127803/how-to-parse-iso-formatted-date-in... + + if '.' in entry: + timestamp_str, microseconds = entry.split('.') + else: + timestamp_str, microseconds = entry, '000000' + + if len(microseconds) != 6 or not microseconds.isdigit(): + raise ValueError("timestamp's microseconds should be six digits") + + timestamp = datetime.datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S") + return timestamp + datetime.timedelta(microseconds = int(microseconds)) + def _get_label(units, count, decimal, is_long): """ Provides label corresponding to units of the highest significance in the diff --git a/stem/util/tor_tools.py b/stem/util/tor_tools.py index c70fbce..3673be9 100644 --- a/stem/util/tor_tools.py +++ b/stem/util/tor_tools.py @@ -7,16 +7,21 @@ Miscellaneous utility functions for working with tor.
is_valid_fingerprint - checks if a string is a valid tor relay fingerprint is_valid_nickname - checks if a string is a valid tor relay nickname + is_valid_circuit_id - checks if a string is a valid tor circuit id is_hex_digits - checks if a string is only made up of hex digits """
import re
# The control-spec defines the following as... +# # Fingerprint = "$" 40*HEXDIG # NicknameChar = "a"-"z" / "A"-"Z" / "0" - "9" # Nickname = 1*19 NicknameChar # +# CircuitID = 1*16 IDChar +# IDChar = ALPHA / DIGIT +# # HEXDIG is defined in RFC 5234 as being uppercase and used in RFC 5987 as # case insensitive. Tor doesn't define this in the spec so flipping a coin # and going with case insensitive. @@ -24,6 +29,7 @@ import re HEX_DIGIT = "[0-9a-fA-F]" FINGERPRINT_PATTERN = re.compile("^%s{40}$" % HEX_DIGIT) NICKNAME_PATTERN = re.compile("^[a-zA-Z0-9]{1,19}$") +CIRC_ID_PATTERN = re.compile("^[a-zA-Z0-9]{1,16}$")
def is_valid_fingerprint(entry, check_prefix = False): """ @@ -59,6 +65,18 @@ def is_valid_nickname(entry):
return bool(NICKNAME_PATTERN.match(entry))
+def is_valid_circuit_id(entry): + """ + Checks if a string is a valid format for being a circuit identifier. + + :returns: **True** if the string could be a circuit id, **False** otherwise + """ + + if not isinstance(entry, str): + return False + + return bool(CIRC_ID_PATTERN.match(entry)) + def is_hex_digits(entry, count): """ Checks if a string is the given number of hex digits. Digits represented by diff --git a/test/unit/__init__.py b/test/unit/__init__.py index 15a6b88..a57679c 100644 --- a/test/unit/__init__.py +++ b/test/unit/__init__.py @@ -4,6 +4,7 @@ Unit tests for the stem library.
__all__ = [ "connection", + "control", "descriptor", "exit_policy", "socket", diff --git a/test/unit/control/__init__.py b/test/unit/control/__init__.py new file mode 100644 index 0000000..448a597 --- /dev/null +++ b/test/unit/control/__init__.py @@ -0,0 +1,6 @@ +""" +Unit tests for stem.control. +""" + +__all__ = ["controller"] + diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py new file mode 100644 index 0000000..e25fbe0 --- /dev/null +++ b/test/unit/control/controller.py @@ -0,0 +1,50 @@ +""" +Unit tests for the stem.control module. The module's primarily exercised via +integ tests, but a few bits lend themselves to unit testing. +""" + +import unittest + +from stem import ProtocolError +from stem.control import _parse_circ_path + +class TestControl(unittest.TestCase): + def test_parse_circ_path(self): + """ + Exercises the _parse_circ_path() helper function. + """ + + # empty input + + self.assertEqual([], _parse_circ_path(None)) + self.assertEqual([], _parse_circ_path('')) + + # check the pydoc examples + + pydoc_examples = { + '$999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz': + [('999A226EBED397F331B612FE1E4CFAE5C1F201BA', 'piyaz')], + '$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14': + [('E57A476CD4DFBD99B4EE52A100A58610AD6E80B9', None), + (None, 'hamburgerphone'), + (None, 'PrivacyRepublic14'), + ], + } + + for test_input, expected in pydoc_examples.items(): + self.assertEqual(expected, _parse_circ_path(test_input)) + + # exercise with some invalid inputs + + malformed_inputs = [ + '=piyaz', # no fingerprint + '999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz', # fingerprint missing prefix + '$999A226EBED397F331B612FE1E4CFAE5C1F201BAA=piyaz', # fingerprint too long + '$999A226EBED397F331B612FE1E4CFAE5C1F201B=piyaz', # fingerprint too short + '$999A226EBED397F331B612FE1E4CFAE5C1F201Bz=piyaz', # invalid character in fingerprint + '$999A226EBED397F331B612FE1E4CFAE5C1F201BA=', # no nickname + ] + + for test_input in malformed_inputs: + self.assertRaises(ProtocolError, _parse_circ_path, test_input) + diff --git a/test/unit/response/events.py b/test/unit/response/events.py index 1942b69..d0c0b4e 100644 --- a/test/unit/response/events.py +++ b/test/unit/response/events.py @@ -2,6 +2,7 @@ Unit tests for the stem.response.events classes. """
+import datetime import threading import unittest
@@ -9,7 +10,37 @@ import stem.response import stem.response.events import test.mocking as mocking
-from stem.socket import ProtocolError +from stem import ProtocolError +from stem.control import CircStatus, CircBuildFlag, CircPurpose, CircClosureReason + +# CIRC events from tor v0.2.3.16 + +CIRC_LAUNCHED = "650 CIRC 7 LAUNCHED \ +BUILD_FLAGS=NEED_CAPACITY \ +PURPOSE=GENERAL \ +TIME_CREATED=2012-11-08T16:48:38.417238" + +CIRC_EXTENDED = "650 CIRC 7 EXTENDED \ +$999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz \ +BUILD_FLAGS=NEED_CAPACITY \ +PURPOSE=GENERAL \ +TIME_CREATED=2012-11-08T16:48:38.417238" + +CIRC_FAILED = "650 CIRC 5 FAILED \ +$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9=ergebnisoffen \ +BUILD_FLAGS=NEED_CAPACITY \ +PURPOSE=GENERAL \ +TIME_CREATED=2012-11-08T16:48:36.400959 \ +REASON=DESTROYED \ +REMOTE_REASON=OR_CONN_CLOSED" + +# CIRC events from tor v0.2.1.30 without the VERBOSE_NAMES feature + +CIRC_LAUNCHED_OLD = "650 CIRC 4 LAUNCHED" +CIRC_EXTENDED_OLD = "650 CIRC 1 EXTENDED \ +$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone" +CIRC_BUILT_OLD = "650 CIRC 1 BUILT \ +$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14"
def _get_event(content): controller_event = mocking.get_message(content) @@ -47,6 +78,97 @@ class TestEvents(unittest.TestCase): time.sleep(0.2) events_thread.join()
+ def test_circ_event(self): + event = _get_event(CIRC_LAUNCHED) + + self.assertTrue(isinstance(event, stem.response.events.CircuitEvent)) + self.assertEqual(CIRC_LAUNCHED.lstrip("650 "), str(event)) + self.assertEqual("7", event.id) + self.assertEqual(CircStatus.LAUNCHED, event.status) + self.assertEqual((), event.path) + self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags) + self.assertEqual(CircPurpose.GENERAL, event.purpose) + self.assertEqual(None, event.hs_state) + self.assertEqual(None, event.rend_query) + self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 38, 417238), event.created) + self.assertEqual(None, event.reason) + self.assertEqual(None, event.remote_reason) + + event = _get_event(CIRC_EXTENDED) + + self.assertTrue(isinstance(event, stem.response.events.CircuitEvent)) + self.assertEqual(CIRC_EXTENDED.lstrip("650 "), str(event)) + self.assertEqual("7", event.id) + self.assertEqual(CircStatus.EXTENDED, event.status) + self.assertEqual((("999A226EBED397F331B612FE1E4CFAE5C1F201BA", "piyaz"),), event.path) + self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags) + self.assertEqual(CircPurpose.GENERAL, event.purpose) + self.assertEqual(None, event.hs_state) + self.assertEqual(None, event.rend_query) + self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 38, 417238), event.created) + self.assertEqual(None, event.reason) + self.assertEqual(None, event.remote_reason) + + event = _get_event(CIRC_FAILED) + + self.assertTrue(isinstance(event, stem.response.events.CircuitEvent)) + self.assertEqual(CIRC_FAILED.lstrip("650 "), str(event)) + self.assertEqual("5", event.id) + self.assertEqual(CircStatus.FAILED, event.status) + self.assertEqual((("E57A476CD4DFBD99B4EE52A100A58610AD6E80B9", "ergebnisoffen"),), event.path) + self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags) + self.assertEqual(CircPurpose.GENERAL, event.purpose) + self.assertEqual(None, event.hs_state) + self.assertEqual(None, event.rend_query) + self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 36, 400959), event.created) + self.assertEqual(CircClosureReason.DESTROYED, event.reason) + self.assertEqual(CircClosureReason.OR_CONN_CLOSED, event.remote_reason) + + event = _get_event(CIRC_LAUNCHED_OLD) + + self.assertTrue(isinstance(event, stem.response.events.CircuitEvent)) + self.assertEqual(CIRC_LAUNCHED_OLD.lstrip("650 "), str(event)) + self.assertEqual("4", event.id) + self.assertEqual(CircStatus.LAUNCHED, event.status) + self.assertEqual((), event.path) + self.assertEqual(None, event.build_flags) + self.assertEqual(None, event.purpose) + self.assertEqual(None, event.hs_state) + self.assertEqual(None, event.rend_query) + self.assertEqual(None, event.created) + self.assertEqual(None, event.reason) + self.assertEqual(None, event.remote_reason) + + event = _get_event(CIRC_EXTENDED_OLD) + + self.assertTrue(isinstance(event, stem.response.events.CircuitEvent)) + self.assertEqual(CIRC_EXTENDED_OLD.lstrip("650 "), str(event)) + self.assertEqual("1", event.id) + self.assertEqual(CircStatus.EXTENDED, event.status) + self.assertEqual((("E57A476CD4DFBD99B4EE52A100A58610AD6E80B9", None), (None,"hamburgerphone")), event.path) + self.assertEqual(None, event.build_flags) + self.assertEqual(None, event.purpose) + self.assertEqual(None, event.hs_state) + self.assertEqual(None, event.rend_query) + self.assertEqual(None, event.created) + self.assertEqual(None, event.reason) + self.assertEqual(None, event.remote_reason) + + event = _get_event(CIRC_BUILT_OLD) + + self.assertTrue(isinstance(event, stem.response.events.CircuitEvent)) + self.assertEqual(CIRC_BUILT_OLD.lstrip("650 "), str(event)) + self.assertEqual("1", event.id) + self.assertEqual(CircStatus.BUILT, event.status) + self.assertEqual((("E57A476CD4DFBD99B4EE52A100A58610AD6E80B9", None), (None,"hamburgerphone"), (None, "PrivacyRepublic14")), event.path) + self.assertEqual(None, event.build_flags) + self.assertEqual(None, event.purpose) + self.assertEqual(None, event.hs_state) + self.assertEqual(None, event.rend_query) + self.assertEqual(None, event.created) + self.assertEqual(None, event.reason) + self.assertEqual(None, event.remote_reason) + def test_bw_event(self): event = _get_event("650 BW 15 25")
diff --git a/test/unit/util/str_tools.py b/test/unit/util/str_tools.py index 3d42154..e91e324 100644 --- a/test/unit/util/str_tools.py +++ b/test/unit/util/str_tools.py @@ -2,7 +2,9 @@ Unit tests for the stem.util.str_tools functions. """
+import datetime import unittest + from stem.util import str_tools
class TestStrTools(unittest.TestCase): @@ -118,4 +120,33 @@ class TestStrTools(unittest.TestCase): self.assertRaises(ValueError, str_tools.parse_short_time_label, '05:') self.assertRaises(ValueError, str_tools.parse_short_time_label, '05a:00') self.assertRaises(ValueError, str_tools.parse_short_time_label, '-05:00') + + def test_parse_iso_timestamp(self): + """ + Checks the parse_iso_timestamp() function. + """ + + test_inputs = { + '2012-11-08T16:48:41.420251': + datetime.datetime(2012, 11, 8, 16, 48, 41, 420251), + '2012-11-08T16:48:41.000000': + datetime.datetime(2012, 11, 8, 16, 48, 41, 0), + '2012-11-08T16:48:41': + datetime.datetime(2012, 11, 8, 16, 48, 41, 0), + } + + for arg, expected in test_inputs.items(): + self.assertEqual(expected, str_tools.parse_iso_timestamp(arg)) + + invalid_input = [ + None, + 32, + 'hello world', + '2012-11-08T16:48:41.42025', # too few microsecond digits + '2012-11-08T16:48:41.4202511', # too many microsecond digits + '2012-11-08T16:48', + ] + + for arg in invalid_input: + self.assertRaises(ValueError, str_tools.parse_iso_timestamp, arg)
tor-commits@lists.torproject.org