commit 26b27b6eff21214b17b4018a16ac97f3bd674734 Author: Damian Johnson atagar@torproject.org Date: Mon Jan 7 00:31:22 2013 -0800
Conforming to E302 (two blank lines above methods and functions)
Another long one. I hope this is almost done... --- run_tests.py | 2 + stem/__init__.py | 9 +++++ stem/connection.py | 34 ++++++++++++++++++++ stem/control.py | 5 +++ stem/descriptor/__init__.py | 6 +++ stem/descriptor/export.py | 3 ++ stem/descriptor/extrainfo_descriptor.py | 5 +++ stem/descriptor/networkstatus.py | 12 +++++++ stem/descriptor/reader.py | 9 +++++ stem/descriptor/router_status_entry.py | 13 +++++++ stem/descriptor/server_descriptor.py | 4 ++ stem/exit_policy.py | 6 +++ stem/prereq.py | 5 +++ stem/process.py | 3 ++ stem/response/__init__.py | 6 +++ stem/response/authchallenge.py | 1 + stem/response/events.py | 20 +++++++++++ stem/response/getconf.py | 1 + stem/response/getinfo.py | 1 + stem/response/mapaddress.py | 1 + stem/response/protocolinfo.py | 1 + stem/socket.py | 6 +++ stem/util/conf.py | 6 +++ stem/util/connection.py | 11 ++++++ stem/util/enum.py | 2 + stem/util/log.py | 15 +++++++++ stem/util/ordereddict.py | 1 + stem/util/proc.py | 14 ++++++++ stem/util/str_tools.py | 8 +++++ stem/util/system.py | 12 +++++++ stem/util/term.py | 1 + stem/util/tor_tools.py | 5 +++ stem/version.py | 3 ++ test/check_whitespace.py | 5 ++- test/integ/connection/authentication.py | 3 ++ test/integ/connection/connect.py | 1 + test/integ/control/base_controller.py | 2 + test/integ/control/controller.py | 1 + test/integ/descriptor/__init__.py | 1 + test/integ/descriptor/extrainfo_descriptor.py | 1 + test/integ/descriptor/networkstatus.py | 1 + test/integ/descriptor/reader.py | 5 +++ test/integ/descriptor/server_descriptor.py | 1 + test/integ/process.py | 2 + test/integ/response/protocolinfo.py | 1 + test/integ/socket/control_message.py | 1 + test/integ/socket/control_socket.py | 1 + test/integ/util/conf.py | 3 ++ test/integ/util/proc.py | 1 + test/integ/util/system.py | 3 ++ test/integ/version.py | 1 + test/mocking.py | 29 +++++++++++++++++ test/network.py | 4 ++ test/output.py | 10 ++++++ test/prompt.py | 5 +++ test/runner.py | 11 ++++++ test/unit/connection/authentication.py | 1 + test/unit/control/controller.py | 1 + test/unit/descriptor/export.py | 1 + test/unit/descriptor/extrainfo_descriptor.py | 1 + .../networkstatus/directory_authority.py | 1 + test/unit/descriptor/networkstatus/document_v2.py | 1 + test/unit/descriptor/networkstatus/document_v3.py | 1 + .../descriptor/networkstatus/key_certificate.py | 1 + test/unit/descriptor/reader.py | 1 + test/unit/descriptor/router_status_entry.py | 1 + test/unit/descriptor/server_descriptor.py | 1 + test/unit/exit_policy/policy.py | 1 + test/unit/exit_policy/rule.py | 1 + test/unit/response/authchallenge.py | 1 + test/unit/response/control_line.py | 1 + test/unit/response/control_message.py | 1 + test/unit/response/events.py | 1 + test/unit/response/getconf.py | 1 + test/unit/response/getinfo.py | 1 + test/unit/response/mapaddress.py | 1 + test/unit/response/protocolinfo.py | 1 + test/unit/response/singleline.py | 1 + test/unit/tutorial.py | 1 + test/unit/util/conf.py | 1 + test/unit/util/connection.py | 1 + test/unit/util/enum.py | 1 + test/unit/util/proc.py | 1 + test/unit/util/str_tools.py | 1 + test/unit/util/system.py | 2 + test/unit/util/tor_tools.py | 1 + test/unit/version.py | 1 + test/util.py | 2 + 88 files changed, 357 insertions(+), 1 deletions(-)
diff --git a/run_tests.py b/run_tests.py index f9b18fc..9774c55 100755 --- a/run_tests.py +++ b/run_tests.py @@ -165,6 +165,7 @@ INTEG_TESTS = ( test.integ.control.controller.TestController, )
+ def load_user_configuration(test_config): """ Parses our commandline arguments, loading our custom test configuration if @@ -254,6 +255,7 @@ def load_user_configuration(test_config): print " TRACE, DEBUG, INFO, NOTICE, WARN, ERROR" sys.exit(1)
+ def _clean_orphaned_pyc(): test.output.print_noline(" checking for orphaned .pyc files... ", *test.runner.STATUS_ATTR)
diff --git a/stem/__init__.py b/stem/__init__.py index f02897c..45bf14d 100644 --- a/stem/__init__.py +++ b/stem/__init__.py @@ -403,12 +403,15 @@ import stem.util.enum
UNDEFINED = "<Undefined_ >"
+ class ControllerError(Exception): "Base error for controller communication issues."
+ class ProtocolError(ControllerError): "Malformed content from the control socket."
+ class OperationFailed(ControllerError): """ Base exception class for failed operations that return an error code @@ -423,11 +426,13 @@ class OperationFailed(ControllerError): self.code = code self.message = message
+ class UnsatisfiableRequest(OperationFailed): """ Exception raised if Tor was unable to process our request. """
+ class CircuitExtensionFailed(UnsatisfiableRequest): """ An attempt to create or extend a circuit failed. @@ -439,11 +444,13 @@ class CircuitExtensionFailed(UnsatisfiableRequest): super(CircuitExtensionFailed, self).__init__(message = message) self.circ = circ
+ class InvalidRequest(OperationFailed): """ Exception raised when the request was invalid or malformed. """
+ class InvalidArguments(InvalidRequest): """ Exception class for requests which had invalid arguments. @@ -458,9 +465,11 @@ class InvalidArguments(InvalidRequest): super(InvalidArguments, self).__init__(code, message) self.arguments = arguments
+ class SocketError(ControllerError): "Error arose while communicating with the control socket."
+ class SocketClosed(SocketError): "Control socket was closed before completing the message."
diff --git a/stem/connection.py b/stem/connection.py index 0291a82..ef7ba04 100644 --- a/stem/connection.py +++ b/stem/connection.py @@ -120,6 +120,7 @@ AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "SAFECOOKIE", "UN CLIENT_HASH_CONSTANT = "Tor safe cookie authentication controller-to-server hash" SERVER_HASH_CONSTANT = "Tor safe cookie authentication server-to-controller hash"
+ def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = None, chroot_path = None, controller = stem.control.Controller): """ Convenience function for quickly getting a control connection. This is very @@ -145,6 +146,7 @@ def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = Non
return _connect(control_port, password, chroot_path, controller)
+ def connect_socket_file(socket_path = "/var/run/tor/control", password = None, chroot_path = None, controller = stem.control.Controller): """ Convenience function for quickly getting a control connection. For more @@ -167,6 +169,7 @@ def connect_socket_file(socket_path = "/var/run/tor/control", password = None, c
return _connect(control_socket, password, chroot_path, controller)
+ def _connect(control_socket, password, chroot_path, controller): """ Common implementation for the connect_* functions. @@ -202,6 +205,7 @@ def _connect(control_socket, password, chroot_path, controller): print "Unable to authenticate: %s" % exc return None
+ def authenticate(controller, password = None, chroot_path = None, protocolinfo_response = None): """ Authenticates to a control socket using the information provided by a @@ -412,6 +416,7 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
raise AssertionError("BUG: Authentication failed without providing a recognized exception: %s" % str(auth_exceptions))
+ def authenticate_none(controller, suppress_ctl_errors = True): """ Authenticates to an open control socket. All control connections need to @@ -458,6 +463,7 @@ def authenticate_none(controller, suppress_ctl_errors = True): else: raise OpenAuthRejected("Socket failed (%s)" % exc)
+ def authenticate_password(controller, password, suppress_ctl_errors = True): """ Authenticates to a control socket that uses a password (via the @@ -527,6 +533,7 @@ def authenticate_password(controller, password, suppress_ctl_errors = True): else: raise PasswordAuthRejected("Socket failed (%s)" % exc)
+ def authenticate_cookie(controller, cookie_path, suppress_ctl_errors = True): """ Authenticates to a control socket that uses the contents of an authentication @@ -605,6 +612,7 @@ def authenticate_cookie(controller, cookie_path, suppress_ctl_errors = True): else: raise CookieAuthRejected("Socket failed (%s)" % exc, cookie_path, False)
+ def authenticate_safecookie(controller, cookie_path, suppress_ctl_errors = True): """ Authenticates to a control socket using the safe cookie method, which is @@ -750,6 +758,7 @@ def authenticate_safecookie(controller, cookie_path, suppress_ctl_errors = True) else: raise CookieAuthRejected(str(auth_response), cookie_path, True, auth_response)
+ def get_protocolinfo(controller): """ Issues a PROTOCOLINFO query to a control socket, getting information about @@ -815,6 +824,7 @@ def get_protocolinfo(controller):
return protocolinfo_response
+ def _msg(controller, message): """ Sends and receives a message with either a @@ -827,6 +837,7 @@ def _msg(controller, message): else: return controller.msg(message)
+ def _read_cookie(cookie_path, is_safecookie): """ Provides the contents of a given cookie file. @@ -868,6 +879,7 @@ def _read_cookie(cookie_path, is_safecookie): exc_msg = "Authentication failed: unable to read '%s' (%s)" % (cookie_path, exc) raise UnreadableCookieFile(exc_msg, cookie_path, is_safecookie)
+ def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg): """ Attempts to expand a relative cookie path with the given pid resolver. This @@ -901,6 +913,7 @@ def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg)
protocolinfo_response.cookie_path = cookie_path
+ class AuthenticationFailure(Exception): """ Base error for authentication failures. @@ -913,6 +926,7 @@ class AuthenticationFailure(Exception): super(AuthenticationFailure, self).__init__(message) self.auth_response = auth_response
+ class UnrecognizedAuthMethods(AuthenticationFailure): """ All methods for authenticating aren't recognized. @@ -924,27 +938,35 @@ class UnrecognizedAuthMethods(AuthenticationFailure): super(UnrecognizedAuthMethods, self).__init__(message) self.unknown_auth_methods = unknown_auth_methods
+ class IncorrectSocketType(AuthenticationFailure): "Socket does not speak the control protocol."
+ class OpenAuthFailed(AuthenticationFailure): "Failure to authenticate to an open socket."
+ class OpenAuthRejected(OpenAuthFailed): "Attempt to connect to an open control socket was rejected."
+ class PasswordAuthFailed(AuthenticationFailure): "Failure to authenticate with a password."
+ class PasswordAuthRejected(PasswordAuthFailed): "Socket does not support password authentication."
+ class IncorrectPassword(PasswordAuthFailed): "Authentication password incorrect."
+ class MissingPassword(PasswordAuthFailed): "Password authentication is supported but we weren't provided with one."
+ class CookieAuthFailed(AuthenticationFailure): """ Failure to authenticate with an authentication cookie. @@ -961,18 +983,23 @@ class CookieAuthFailed(AuthenticationFailure): self.is_safecookie = is_safecookie self.cookie_path = cookie_path
+ class CookieAuthRejected(CookieAuthFailed): "Socket does not support password authentication."
+ class IncorrectCookieValue(CookieAuthFailed): "Authentication cookie value was rejected."
+ class IncorrectCookieSize(CookieAuthFailed): "Aborted because the cookie file is the wrong size."
+ class UnreadableCookieFile(CookieAuthFailed): "Error arose in reading the authentication cookie."
+ class AuthChallengeFailed(CookieAuthFailed): """ AUTHCHALLENGE command has failed. @@ -981,11 +1008,13 @@ class AuthChallengeFailed(CookieAuthFailed): def __init__(self, message, cookie_path): super(AuthChallengeFailed, self).__init__(message, cookie_path, True)
+ class AuthChallengeUnsupported(AuthChallengeFailed): """ AUTHCHALLENGE isn't implemented. """
+ class UnrecognizedAuthChallengeMethod(AuthChallengeFailed): """ Tor couldn't recognize our AUTHCHALLENGE method. @@ -997,21 +1026,26 @@ class UnrecognizedAuthChallengeMethod(AuthChallengeFailed): super(UnrecognizedAuthChallengeMethod, self).__init__(message, cookie_path) self.authchallenge_method = authchallenge_method
+ class AuthSecurityFailure(AuthChallengeFailed): "AUTHCHALLENGE response is invalid."
+ class InvalidClientNonce(AuthChallengeFailed): "AUTHCHALLENGE request contains an invalid client nonce."
+ class MissingAuthInfo(AuthenticationFailure): """ The PROTOCOLINFO response didn't have enough information to authenticate. These are valid control responses but really shouldn't happen in practice. """
+ class NoAuthMethods(MissingAuthInfo): "PROTOCOLINFO response didn't have any methods for authenticating."
+ class NoAuthCookie(MissingAuthInfo): """ PROTOCOLINFO response supports cookie auth but doesn't have its path. diff --git a/stem/control.py b/stem/control.py index feb7834..2e82a65 100644 --- a/stem/control.py +++ b/stem/control.py @@ -210,6 +210,7 @@ GEOIP_FAILURE_THRESHOLD = 5 # changed to the more conventional is_alive() and current_thread() in python # 2.6 and above. We should use that when dropping python 2.5 compatibility.
+ class BaseController(object): """ Controller for the tor process. This is a minimal base class for other @@ -584,6 +585,7 @@ class BaseController(object): self._event_notice.wait() self._event_notice.clear()
+ class Controller(BaseController): """ Communicates with a control socket. This is built on top of the @@ -1920,6 +1922,7 @@ class Controller(BaseController): if not response.is_ok(): raise stem.ProtocolError("SETEVENTS received unexpected response\n%s" % response)
+ def _parse_circ_path(path): """ Parses a circuit path as a list of **(fingerprint, nickname)** tuples. Tor @@ -1962,6 +1965,7 @@ def _parse_circ_path(path): else: return []
+ def _parse_circ_entry(entry): """ Parses a single relay's 'LongName' or 'ServerID'. See the @@ -1998,6 +2002,7 @@ def _parse_circ_entry(entry):
return (fingerprint, nickname)
+ def _case_insensitive_lookup(entries, key, default = UNDEFINED): """ Makes a case insensitive lookup within a list or dictionary, providing the diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py index 0dba96c..97dc94a 100644 --- a/stem/descriptor/__init__.py +++ b/stem/descriptor/__init__.py @@ -55,6 +55,7 @@ Flag = stem.util.enum.Enum( ("VALID", "Valid"), )
+ def parse_file(path, descriptor_file): """ Provides an iterator for the descriptors within a given file. @@ -111,6 +112,7 @@ def parse_file(path, descriptor_file):
raise TypeError("Unable to determine the descriptor's type. filename: '%s', first line: '%s'" % (filename, first_line))
+ def _parse_metrics_file(descriptor_type, major_version, minor_version, descriptor_file): # Parses descriptor files from metrics, yielding individual descriptors. This # throws a TypeError if the descriptor_type or version isn't recognized. @@ -141,6 +143,7 @@ def _parse_metrics_file(descriptor_type, major_version, minor_version, descripto else: raise TypeError("Unrecognized metrics descriptor format. type: '%s', version: '%i.%i'" % (descriptor_type, major_version, minor_version))
+ class Descriptor(object): """ Common parent for all types of descriptors. @@ -176,6 +179,7 @@ class Descriptor(object): def __str__(self): return self._raw_contents
+ def _read_until_keywords(keywords, descriptor_file, inclusive = False, ignore_first = False, skip = False, end_position = None, include_ending_keyword = False): """ Reads from the descriptor file until we get to one of the given keywords or reach the @@ -241,6 +245,7 @@ def _read_until_keywords(keywords, descriptor_file, inclusive = False, ignore_fi else: return content
+ def _get_pseudo_pgp_block(remaining_contents): """ Checks if given contents begins with a pseudo-Open-PGP-style block and, if @@ -276,6 +281,7 @@ def _get_pseudo_pgp_block(remaining_contents): else: return None
+ def _get_descriptor_components(raw_contents, validate, extra_keywords = ()): """ Initial breakup of the server descriptor contents to make parsing easier. diff --git a/stem/descriptor/export.py b/stem/descriptor/export.py index becc72f..5be7dbc 100644 --- a/stem/descriptor/export.py +++ b/stem/descriptor/export.py @@ -15,9 +15,11 @@ import csv import stem.descriptor import stem.prereq
+ class _ExportDialect(csv.excel): lineterminator = '\n'
+ def export_csv(descriptors, included_fields = (), excluded_fields = (), header = True): """ Provides a newline separated CSV for one or more descriptors. If simply @@ -40,6 +42,7 @@ def export_csv(descriptors, included_fields = (), excluded_fields = (), header = export_csv_file(output_buffer, descriptors, included_fields, excluded_fields, header) return output_buffer.getvalue()
+ def export_csv_file(output_file, descriptors, included_fields = (), excluded_fields = (), header = True): """ Similar to :func:`stem.descriptor.export.export_csv`, except that the CSV is diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py index 2c14c92..25ab615 100644 --- a/stem/descriptor/extrainfo_descriptor.py +++ b/stem/descriptor/extrainfo_descriptor.py @@ -133,6 +133,7 @@ SINGLE_FIELDS = ( "exit-streams-opened", )
+ def parse_file(descriptor_file, validate = True): """ Iterates over the extra-info descriptors in a file. @@ -161,6 +162,7 @@ def parse_file(descriptor_file, validate = True): else: break # done parsing file
+ def _parse_timestamp_and_interval(keyword, content): """ Parses a 'YYYY-MM-DD HH:MM:SS (NSEC s) *' entry. @@ -194,6 +196,7 @@ def _parse_timestamp_and_interval(keyword, content): except ValueError: raise ValueError("%s line's timestamp wasn't parsable: %s" % (keyword, line))
+ class ExtraInfoDescriptor(stem.descriptor.Descriptor): """ Extra-info descriptor document. @@ -791,6 +794,7 @@ class ExtraInfoDescriptor(stem.descriptor.Descriptor): def _last_keyword(self): return "router-signature"
+ class RelayExtraInfoDescriptor(ExtraInfoDescriptor): """ Relay extra-info descriptor, constructed from data such as that provided by @@ -838,6 +842,7 @@ class RelayExtraInfoDescriptor(ExtraInfoDescriptor):
ExtraInfoDescriptor._parse(self, entries, validate)
+ class BridgeExtraInfoDescriptor(ExtraInfoDescriptor): """ Bridge extra-info descriptor (`bridge descriptor specification diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py index 28537c2..e8e3ae2 100644 --- a/stem/descriptor/networkstatus.py +++ b/stem/descriptor/networkstatus.py @@ -165,6 +165,7 @@ BANDWIDTH_WEIGHT_ENTRIES = ( "Wmb", "Wmd", "Wme", "Wmg", "Wmm", )
+ def parse_file(document_file, validate = True, is_microdescriptor = False, document_version = 3): """ Parses a network status and iterates over the RouterStatusEntry in it. The @@ -223,6 +224,7 @@ def parse_file(document_file, validate = True, is_microdescriptor = False, docum for desc in desc_iterator: yield desc
+ class NetworkStatusDocument(stem.descriptor.Descriptor): """ Common parent for network status documents. @@ -235,6 +237,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor): def get_unrecognized_lines(self): return list(self._unrecognized_lines)
+ class NetworkStatusDocumentV2(NetworkStatusDocument): """ Version 2 network status document. These have been deprecated and are no @@ -406,6 +409,7 @@ class NetworkStatusDocumentV2(NetworkStatusDocument): if 'network-status-version' != entries.keys()[0]: raise ValueError("Network status document (v2) are expected to start with a 'network-status-version' line:\n%s" % str(self))
+ class NetworkStatusDocumentV3(NetworkStatusDocument): """ Version 3 network status document. This could be either a vote or consensus. @@ -524,6 +528,7 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
return str(self) > str(other)
+ class _DocumentHeader(object): def __init__(self, document_file, validate, default_params): self.version = None @@ -733,6 +738,7 @@ class _DocumentHeader(object): if value < minimum or value > maximum: raise ValueError("'%s' value on the params line must be in the range of %i - %i, was %i" % (key, minimum, maximum, value))
+ class _DocumentFooter(object): def __init__(self, document_file, validate, header): self.signatures = [] @@ -796,6 +802,7 @@ class _DocumentFooter(object):
self.signatures.append(DocumentSignature(method, fingerprint, key_digest, block_contents, validate))
+ def _check_for_missing_and_disallowed_fields(header, entries, fields): """ Checks that we have mandatory fields for our type, and that we don't have @@ -828,6 +835,7 @@ def _check_for_missing_and_disallowed_fields(header, entries, fields): if disallowed_fields: raise ValueError("Network status document has fields that shouldn't appear in this document type or version: %s" % ', '.join(disallowed_fields))
+ def _check_for_misordered_fields(entries, expected): """ To be valid a network status document's fiends need to appear in a specific @@ -857,6 +865,7 @@ def _check_for_misordered_fields(entries, expected): expected_label = ', '.join(expected) raise ValueError("The fields in a section of the document are misordered. It should be '%s' but was '%s'" % (actual_label, expected_label))
+ def _parse_int_mappings(keyword, value, validate): # Parse a series of 'key=value' entries, checking the following: # - values are integers @@ -895,6 +904,7 @@ def _parse_int_mappings(keyword, value, validate):
return results
+ class DirectoryAuthority(stem.descriptor.Descriptor): """ Directory authority information obtained from a v3 network status document. @@ -1085,6 +1095,7 @@ class DirectoryAuthority(stem.descriptor.Descriptor):
return str(self) > str(other)
+ class KeyCertificate(stem.descriptor.Descriptor): """ Directory key certificate for a v3 network status document. @@ -1245,6 +1256,7 @@ class KeyCertificate(stem.descriptor.Descriptor):
return str(self) > str(other)
+ class DocumentSignature(object): """ Directory signature of a v3 network status document. diff --git a/stem/descriptor/reader.py b/stem/descriptor/reader.py index 9f9f505..3373a1e 100644 --- a/stem/descriptor/reader.py +++ b/stem/descriptor/reader.py @@ -93,9 +93,11 @@ FINISHED = "DONE" # dropping python 2.5 compatibility... # http://docs.python.org/library/threading.html#threading.Event.is_set
+ class FileSkipped(Exception): "Base error when we can't provide descriptor data from a file."
+ class AlreadyRead(FileSkipped): """ Already read a file with this 'last modified' timestamp or later. @@ -110,6 +112,7 @@ class AlreadyRead(FileSkipped): self.last_modified = last_modified self.last_modified_when_read = last_modified_when_read
+ class ParsingFailure(FileSkipped): """ File contents could not be parsed as descriptor data. @@ -121,6 +124,7 @@ class ParsingFailure(FileSkipped): super(ParsingFailure, self).__init__() self.exception = parsing_exception
+ class UnrecognizedType(FileSkipped): """ File doesn't contain descriptor data. This could either be due to its file @@ -133,6 +137,7 @@ class UnrecognizedType(FileSkipped): super(UnrecognizedType, self).__init__() self.mime_type = mime_type
+ class ReadFailed(FileSkipped): """ An IOError occurred while trying to read the file. @@ -145,12 +150,14 @@ class ReadFailed(FileSkipped): super(ReadFailed, self).__init__() self.exception = read_exception
+ class FileMissing(ReadFailed): "File does not exist."
def __init__(self): super(FileMissing, self).__init__(None)
+ def load_processed_files(path): """ Loads a dictionary of 'path => last modified timestamp' mappings, as @@ -190,6 +197,7 @@ def load_processed_files(path):
return processed_files
+ def save_processed_files(path, processed_files): """ Persists a dictionary of 'path => last modified timestamp' mappings (as @@ -222,6 +230,7 @@ def save_processed_files(path, processed_files):
output_file.write("%s %i\n" % (path, timestamp))
+ class DescriptorReader(object): """ Iterator for the descriptor data on the local file system. This can process diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py index 6e048dd..0479bbd 100644 --- a/stem/descriptor/router_status_entry.py +++ b/stem/descriptor/router_status_entry.py @@ -22,6 +22,7 @@ import datetime import stem.descriptor import stem.exit_policy
+ def parse_file(document_file, validate, entry_class, entry_keyword = "r", start_position = None, end_position = None, section_end_keywords = (), extra_args = ()): """ Reads a range of the document_file containing some number of entry_class @@ -88,6 +89,7 @@ def parse_file(document_file, validate, entry_class, entry_keyword = "r", start_ else: break
+ class RouterStatusEntry(stem.descriptor.Descriptor): """ Information about an individual router stored within a network status @@ -226,6 +228,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
return str(self) > str(other)
+ class RouterStatusEntryV2(RouterStatusEntry): """ Information about an individual router stored within a version 2 network @@ -269,6 +272,7 @@ class RouterStatusEntryV2(RouterStatusEntry):
return str(self) > str(other)
+ class RouterStatusEntryV3(RouterStatusEntry): """ Information about an individual router stored within a version 3 network @@ -350,6 +354,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
return str(self) > str(other)
+ class RouterStatusEntryMicroV3(RouterStatusEntry): """ Information about an individual router stored within a microdescriptor @@ -411,6 +416,7 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
return str(self) > str(other)
+ def _parse_r_line(desc, value, validate, include_digest = True): # Parses a RouterStatusEntry's 'r' line. They're very nearly identical for # all current entry types (v2, v3, and microdescriptor v3) with one little @@ -466,6 +472,7 @@ def _parse_r_line(desc, value, validate, include_digest = True): if validate: raise ValueError("Publication time time wasn't parsable: r %s" % value)
+ def _parse_a_line(desc, value, validate): # "a" SP address ":" portlist # example: a [2001:888:2133:0:82:94:251:204]:9001 @@ -499,6 +506,7 @@ def _parse_a_line(desc, value, validate):
desc.addresses_v6.setdefault(address, []).append((int(min_port), int(max_port)))
+ def _parse_s_line(desc, value, validate): # "s" Flags # example: s Named Running Stable Valid @@ -513,6 +521,7 @@ def _parse_s_line(desc, value, validate): elif flag == "": raise ValueError("%s had extra whitespace on its 's' line: s %s" % (desc._name(), value))
+ def _parse_v_line(desc, value, validate): # "v" version # example: v Tor 0.2.2.35 @@ -530,6 +539,7 @@ def _parse_v_line(desc, value, validate): if validate: raise ValueError("%s has a malformed tor version (%s): v %s" % (desc._name(), exc, value))
+ def _parse_w_line(desc, value, validate): # "w" "Bandwidth=" INT ["Measured=" INT] # example: w Bandwidth=7980 @@ -572,6 +582,7 @@ def _parse_w_line(desc, value, validate): else: desc.unrecognized_bandwidth_entries.append(w_entry)
+ def _parse_p_line(desc, value, validate): # "p" ("accept" / "reject") PortList # p reject 1-65535 @@ -585,6 +596,7 @@ def _parse_p_line(desc, value, validate):
raise ValueError("%s exit policy is malformed (%s): p %s" % (desc._name(), exc, value))
+ def _parse_m_line(desc, value, validate): # "m" methods 1*(algorithm "=" digest) # example: m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs @@ -625,6 +637,7 @@ def _parse_m_line(desc, value, validate):
desc.microdescriptor_hashes.append((methods, hashes))
+ def _decode_fingerprint(identity, validate): """ Decodes the 'identity' value found in consensuses into the more common hex diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index 037a4bd..6fe0e7c 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -71,6 +71,7 @@ SINGLE_FIELDS = ( "ntor-onion-key", )
+ def parse_file(descriptor_file, validate = True): """ Iterates over the server descriptors in a file. @@ -129,6 +130,7 @@ def parse_file(descriptor_file, validate = True): else: break # done parsing descriptors
+ class ServerDescriptor(stem.descriptor.Descriptor): """ Common parent for server descriptors. @@ -607,6 +609,7 @@ class ServerDescriptor(stem.descriptor.Descriptor): def _last_keyword(self): return "router-signature"
+ class RelayDescriptor(ServerDescriptor): """ Server descriptor (`descriptor specification @@ -806,6 +809,7 @@ class RelayDescriptor(ServerDescriptor):
return key_bytes
+ class BridgeDescriptor(ServerDescriptor): """ Bridge descriptor (`bridge descriptor specification diff --git a/stem/exit_policy.py b/stem/exit_policy.py index 56c68dc..d82892f 100644 --- a/stem/exit_policy.py +++ b/stem/exit_policy.py @@ -75,6 +75,7 @@ AddressType = stem.util.enum.Enum(("WILDCARD", "Wildcard"), ("IPv4", "IPv4"), (" # some use cases where we might want to construct custom policies. Maybe make # it a CustomExitPolicyRule subclass?
+ class ExitPolicy(object): """ Policy for the destinations that a relay allows or denies exiting to. This @@ -259,6 +260,7 @@ class ExitPolicy(object): else: return False
+ class MicroExitPolicy(ExitPolicy): """ Exit policy provided by the microdescriptors. This is a distilled version of @@ -338,6 +340,7 @@ class MicroExitPolicy(ExitPolicy): else: return False
+ class ExitPolicyRule(object): """ Single rule from the user's exit policy. These rules are chained together to @@ -698,12 +701,15 @@ class ExitPolicyRule(object): else: return False
+ def _address_type_to_int(address_type): return AddressType.index_of(address_type)
+ def _int_to_address_type(address_type_int): return AddressType[AddressType.keys()[address_type_int]]
+ class MicroExitPolicyRule(ExitPolicyRule): """ Lighter weight ExitPolicyRule derivative for microdescriptors. diff --git a/stem/prereq.py b/stem/prereq.py index e3b4fdc..f9e53a7 100644 --- a/stem/prereq.py +++ b/stem/prereq.py @@ -26,6 +26,7 @@ from stem.util import log
IS_CRYPTO_AVAILABLE = None
+ def check_requirements(): """ Checks that we meet the minimum requirements to run stem. If we don't then @@ -41,6 +42,7 @@ def check_requirements(): elif major_version < 2 or minor_version < 5: raise ImportError("stem requires python version 2.5 or greater")
+ def is_python_26(): """ Checks if we're in the 2.6 - 2.x range. @@ -50,6 +52,7 @@ def is_python_26():
return _check_version(6)
+ def is_python_27(): """ Checks if we're in the 2.7 - 2.x range. @@ -59,6 +62,7 @@ def is_python_27():
return _check_version(7)
+ def is_crypto_available(): global IS_CRYPTO_AVAILABLE
@@ -77,6 +81,7 @@ def is_crypto_available():
return IS_CRYPTO_AVAILABLE
+ def _check_version(minor_req): major_version, minor_version = sys.version_info[0:2]
diff --git a/stem/process.py b/stem/process.py index e654e85..64ee591 100644 --- a/stem/process.py +++ b/stem/process.py @@ -29,6 +29,7 @@ import stem.util.system NO_TORRC = "<no torrc>" DEFAULT_INIT_TIMEOUT = 90
+ def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT, take_ownership = False): """ Initializes a tor process. This blocks until initialization completes or we @@ -170,6 +171,7 @@ def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_perce
last_problem = msg
+ def launch_tor_with_config(config, tor_cmd = "tor", completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT, take_ownership = False): """ Initializes a tor process, like :func:`~stem.process.launch_tor`, but with a @@ -230,5 +232,6 @@ def launch_tor_with_config(config, tor_cmd = "tor", completion_percent = 100, in except: pass
+ def _get_pid(): return str(os.getpid()) diff --git a/stem/response/__init__.py b/stem/response/__init__.py index dbf50f9..9544d71 100644 --- a/stem/response/__init__.py +++ b/stem/response/__init__.py @@ -53,6 +53,7 @@ KEY_ARG = re.compile("^(\S+)=") CONTROL_ESCAPES = {r"\": "\", r""": """, r"'": "'", r"\r": "\r", r"\n": "\n", r"\t": "\t"}
+ def convert(response_type, message, **kwargs): """ Converts a :class:`~stem.response.ControlMessage` into a particular kind of @@ -115,6 +116,7 @@ def convert(response_type, message, **kwargs): message.__class__ = response_class message._parse_message(**kwargs)
+ class ControlMessage(object): """ Message from the control socket. This is iterable and can be stringified for @@ -226,6 +228,7 @@ class ControlMessage(object):
return ControlLine(self._parsed_content[index][2])
+ class ControlLine(str): """ String subclass that represents a line of controller output. This behaves as @@ -384,6 +387,7 @@ class ControlLine(str): self._remainder = remainder return (key, next_entry)
+ def _parse_entry(line, quoted, escaped): """ Parses the next entry from the given space separated content. @@ -425,6 +429,7 @@ def _parse_entry(line, quoted, escaped):
return (next_entry, remainder.lstrip())
+ def _get_quote_indices(line, escaped): """ Provides the indices of the next two quotes in the given content. @@ -450,6 +455,7 @@ def _get_quote_indices(line, escaped):
return tuple(indices)
+ class SingleLineResponse(ControlMessage): """ Reply to a request that performs an action rather than querying data. These diff --git a/stem/response/authchallenge.py b/stem/response/authchallenge.py index cdc0ad4..161dd84 100644 --- a/stem/response/authchallenge.py +++ b/stem/response/authchallenge.py @@ -4,6 +4,7 @@ import stem.response import stem.socket import stem.util.tor_tools
+ class AuthChallengeResponse(stem.response.ControlMessage): """ AUTHCHALLENGE query response. diff --git a/stem/response/events.py b/stem/response/events.py index 51aa3d2..e9c3fdc 100644 --- a/stem/response/events.py +++ b/stem/response/events.py @@ -17,6 +17,7 @@ from stem.util import connection, log, str_tools, tor_tools KW_ARG = re.compile("^(.*) ([A-Za-z0-9_]+)=(\S*)$") QUOTED_KW_ARG = re.compile("^(.*) ([A-Za-z0-9_]+)="(.*)"$")
+ class Event(stem.response.ControlMessage): """ Base for events we receive asynchronously, as described in section 4.1 of the @@ -141,6 +142,7 @@ class Event(stem.response.ControlMessage): unrecognized_msg = "%s event had an unrecognized %s (%s). Maybe a new addition to the control protocol? Full Event: '%s'" % (self.type, attr, value, self) log.log_once(log_id, log.INFO, unrecognized_msg)
+ class AddrMapEvent(Event): """ Event that indicates a new address mapping. @@ -174,6 +176,7 @@ class AddrMapEvent(Event): if self.utc_expiry is not None: self.utc_expiry = datetime.datetime.strptime(self.utc_expiry, "%Y-%m-%d %H:%M:%S")
+ class AuthDirNewDescEvent(Event): """ Event specific to directory authorities, indicating that we just received new @@ -202,6 +205,7 @@ class AuthDirNewDescEvent(Event): self.message = lines[2] self.descriptor = '\n'.join(lines[3:-1])
+ class BandwidthEvent(Event): """ Event emitted every second with the bytes sent and received by tor. @@ -226,6 +230,7 @@ class BandwidthEvent(Event): self.read = long(self.read) self.written = long(self.written)
+ class BuildTimeoutSetEvent(Event): """ Event indicating that the timeout value for a circuit has changed. This was @@ -280,6 +285,7 @@ class BuildTimeoutSetEvent(Event):
self._log_if_unrecognized('set_type', stem.TimeoutSetType)
+ class CircuitEvent(Event): """ Event that indicates that a circuit has changed. @@ -338,6 +344,7 @@ class CircuitEvent(Event): self._log_if_unrecognized('reason', stem.CircClosureReason) self._log_if_unrecognized('remote_reason', stem.CircClosureReason)
+ class CircMinorEvent(Event): """ Event providing information about minor changes in our circuits. This was @@ -393,6 +400,7 @@ class CircMinorEvent(Event): self._log_if_unrecognized('old_purpose', stem.CircPurpose) self._log_if_unrecognized('old_hs_state', stem.HiddenServiceState)
+ class ClientsSeenEvent(Event): """ Periodic event on bridge relays that provides a summary of our users. @@ -451,6 +459,7 @@ class ClientsSeenEvent(Event):
self.ip_versions = protocol_to_count
+ class ConfChangedEvent(Event): """ Event that indicates that our configuration changed, either in response to a @@ -485,6 +494,7 @@ class ConfChangedEvent(Event):
self.config[key] = value
+ class DescChangedEvent(Event): """ Event that indicates that our descriptor has changed. @@ -496,6 +506,7 @@ class DescChangedEvent(Event):
pass
+ class GuardEvent(Event): """ Event that indicates that our guard relays have changed. @@ -516,6 +527,7 @@ class GuardEvent(Event):
_POSITIONAL_ARGS = ("guard_type", "name", "status")
+ class LogEvent(Event): """ Tor logging event. These are the most visible kind of event since, by @@ -539,6 +551,7 @@ class LogEvent(Event):
self.message = str(self)[len(self.runlevel) + 1:].rstrip("\nOK")
+ class NetworkStatusEvent(Event): """ Event for when our copy of the consensus has changed. This was introduced in @@ -561,6 +574,7 @@ class NetworkStatusEvent(Event): entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3, ))
+ class NewConsensusEvent(Event): """ Event for when we have a new consensus. This is similar to @@ -585,6 +599,7 @@ class NewConsensusEvent(Event): entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3, ))
+ class NewDescEvent(Event): """ Event that indicates that a new descriptor is available. @@ -603,6 +618,7 @@ class NewDescEvent(Event): def _parse(self): self.relays = tuple([stem.control._parse_circ_entry(entry) for entry in str(self).split()[1:]])
+ class ORConnEvent(Event): """ Event that indicates a change in a relay connection. The 'endpoint' could be @@ -664,6 +680,7 @@ class ORConnEvent(Event): self._log_if_unrecognized('status', stem.ORStatus) self._log_if_unrecognized('reason', stem.ORClosureReason)
+ class SignalEvent(Event): """ Event that indicates that tor has received and acted upon a signal being sent @@ -696,6 +713,7 @@ class SignalEvent(Event):
self._log_if_unrecognized('signal', expected_signals)
+ class StatusEvent(Event): """ Notification of a change in tor's state. These are generally triggered for @@ -726,6 +744,7 @@ class StatusEvent(Event):
self._log_if_unrecognized('runlevel', stem.Runlevel)
+ class StreamEvent(Event): """ Event that indicates that a stream has changed. @@ -796,6 +815,7 @@ class StreamEvent(Event): self._log_if_unrecognized('remote_reason', stem.StreamClosureReason) self._log_if_unrecognized('purpose', stem.StreamPurpose)
+ class StreamBwEvent(Event): """ Event (emitted approximately every second) with the bytes sent and received diff --git a/stem/response/getconf.py b/stem/response/getconf.py index c8b7da3..fff5380 100644 --- a/stem/response/getconf.py +++ b/stem/response/getconf.py @@ -1,6 +1,7 @@ import stem.response import stem.socket
+ class GetConfResponse(stem.response.ControlMessage): """ Reply for a GETCONF query. diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py index 3ef418b..3fcf6c3 100644 --- a/stem/response/getinfo.py +++ b/stem/response/getinfo.py @@ -1,6 +1,7 @@ import stem.response import stem.socket
+ class GetInfoResponse(stem.response.ControlMessage): """ Reply for a GETINFO query. diff --git a/stem/response/mapaddress.py b/stem/response/mapaddress.py index d30ccfa..528f3ad 100644 --- a/stem/response/mapaddress.py +++ b/stem/response/mapaddress.py @@ -1,6 +1,7 @@ import stem.response import stem.socket
+ class MapAddressResponse(stem.response.ControlMessage): """ Reply for a MAPADDRESS query. diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py index 65a2d02..ba841d7 100644 --- a/stem/response/protocolinfo.py +++ b/stem/response/protocolinfo.py @@ -5,6 +5,7 @@ import stem.version from stem.connection import AuthMethod from stem.util import log
+ class ProtocolInfoResponse(stem.response.ControlMessage): """ Version one PROTOCOLINFO query response. diff --git a/stem/socket.py b/stem/socket.py index b23b247..ac21429 100644 --- a/stem/socket.py +++ b/stem/socket.py @@ -39,6 +39,7 @@ import stem.response
from stem.util import log
+ class ControlSocket(object): """ Wrapper for a socket connection that speaks the Tor control protocol. To the @@ -282,6 +283,7 @@ class ControlSocket(object):
raise NotImplementedError("Unsupported Operation: this should be implemented by the ControlSocket subclass")
+ class ControlPort(ControlSocket): """ Control connection to tor. For more information see tor's ControlPort torrc @@ -336,6 +338,7 @@ class ControlPort(ControlSocket): except socket.error, exc: raise stem.SocketError(exc)
+ class ControlSocketFile(ControlSocket): """ Control connection to tor. For more information see tor's ControlSocket torrc @@ -379,6 +382,7 @@ class ControlSocketFile(ControlSocket): except socket.error, exc: raise stem.SocketError(exc)
+ def send_message(control_file, message, raw = False): """ Sends a message to the control socket, adding the expected formatting for @@ -438,6 +442,7 @@ def send_message(control_file, message, raw = False): log.info("Failed to send message: file has been closed") raise stem.SocketClosed("file has been closed")
+ def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or @@ -557,6 +562,7 @@ def recv_message(control_file): log.warn(prefix + ""%s" isn't a recognized divider type" % line) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, line))
+ def send_formatting(message): """ Performs the formatting expected from sent control messages. For more diff --git a/stem/util/conf.py b/stem/util/conf.py index 57f1fda..7e4cc8d 100644 --- a/stem/util/conf.py +++ b/stem/util/conf.py @@ -161,6 +161,7 @@ from stem.util import log
CONFS = {} # mapping of identifier to singleton instances of configs
+ class _SyncListener(object): def __init__(self, config_dict, interceptor): self.config_dict = config_dict @@ -181,6 +182,7 @@ class _SyncListener(object):
self.config_dict[key] = new_value
+ def config_dict(handle, conf_mappings, handler = None): """ Makes a dictionary that stays synchronized with a configuration. @@ -214,6 +216,7 @@ def config_dict(handle, conf_mappings, handler = None): selected_config.add_listener(_SyncListener(conf_mappings, handler).update) return conf_mappings
+ def get_config(handle): """ Singleton constructor for configuration file instances. If a configuration @@ -228,6 +231,7 @@ def get_config(handle):
return CONFS[handle]
+ def parse_enum(key, value, enumeration): """ Provides the enumeration value for a given key. This is a case insensitive @@ -244,6 +248,7 @@ def parse_enum(key, value, enumeration):
return parse_enum_csv(key, value, enumeration, 1)[0]
+ def parse_enum_csv(key, value, enumeration, count = None): """ Parses a given value as being a comma separated listing of enumeration keys, @@ -303,6 +308,7 @@ def parse_enum_csv(key, value, enumeration, count = None):
return result
+ class Config(object): """ Handler for easily working with custom configurations, providing persistence diff --git a/stem/util/connection.py b/stem/util/connection.py index 6892bed..2a9c529 100644 --- a/stem/util/connection.py +++ b/stem/util/connection.py @@ -30,6 +30,7 @@ CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE = os.urandom(32) FULL_IPv4_MASK = "255.255.255.255" FULL_IPv6_MASK = "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"
+ def is_valid_ip_address(address): """ Checks if a string is a valid IPv4 address. @@ -56,6 +57,7 @@ def is_valid_ip_address(address):
return True
+ def is_valid_ipv6_address(address, allow_brackets = False): """ Checks if a string is a valid IPv6 address. @@ -89,6 +91,7 @@ def is_valid_ipv6_address(address, allow_brackets = False):
return True
+ def is_valid_port(entry, allow_zero = False): """ Checks if a string or int is a valid port number. @@ -118,6 +121,7 @@ def is_valid_port(entry, allow_zero = False):
return entry > 0 and entry < 65536
+ def expand_ipv6_address(address): """ Expands abbreviated IPv6 addresses to their full colon separated hex format. @@ -156,6 +160,7 @@ def expand_ipv6_address(address):
return address
+ def get_mask(bits): """ Provides the IPv4 mask for a given number of bits, in the dotted-quad format. @@ -181,6 +186,7 @@ def get_mask(bits): # converts each octet into its integer value return ".".join([str(int(octet, 2)) for octet in octets])
+ def get_masked_bits(mask): """ Provides the number of bits that an IPv4 subnet mask represents. Note that @@ -205,6 +211,7 @@ def get_masked_bits(mask): else: raise ValueError("Unable to convert mask to a bit count: %s" % mask)
+ def get_mask_ipv6(bits): """ Provides the IPv6 mask for a given number of bits, in the hex colon-delimited @@ -231,6 +238,7 @@ def get_mask_ipv6(bits): # converts each group into its hex value return ":".join(["%04x" % int(group, 2) for group in groupings]).upper()
+ def get_binary(value, bits): """ Provides the given value as a binary string, padded with zeros to the given @@ -243,6 +251,7 @@ def get_binary(value, bits): # http://www.daniweb.com/code/snippet216539.html return "".join([str((value >> y) & 1) for y in range(bits - 1, -1, -1)])
+ def get_address_binary(address): """ Provides the binary value for an IPv4 or IPv6 address. @@ -260,6 +269,7 @@ def get_address_binary(address): else: raise ValueError("'%s' is neither an IPv4 or IPv6 address" % address)
+ def hmac_sha256(key, msg): """ Generates a sha256 digest using the given key and message. @@ -272,6 +282,7 @@ def hmac_sha256(key, msg):
return hmac.new(key, msg, hashlib.sha256).digest()
+ def cryptovariables_equal(x, y): """ Compares two strings for equality securely. diff --git a/stem/util/enum.py b/stem/util/enum.py index d22cd22..4617077 100644 --- a/stem/util/enum.py +++ b/stem/util/enum.py @@ -39,6 +39,7 @@ constructed as simple type listings...
import stem.util.str_tools
+ def UppercaseEnum(*args): """ Provides an :class:`~stem.util.enum.Enum` instance where the values are @@ -59,6 +60,7 @@ def UppercaseEnum(*args):
return Enum(*[(v, v) for v in args])
+ class Enum(object): """ Basic enumeration. diff --git a/stem/util/log.py b/stem/util/log.py index 14b09df..04bcbaf 100644 --- a/stem/util/log.py +++ b/stem/util/log.py @@ -77,6 +77,7 @@ DEDUPLICATION_MESSAGE_IDS = set() # could be found for logger "stem"' warning as per... # http://docs.python.org/release/3.1.3/library/logging.html#configuring-loggin...
+ class _NullHandler(logging.Handler): def emit(self, record): pass @@ -84,6 +85,7 @@ class _NullHandler(logging.Handler): if not LOGGER.handlers: LOGGER.addHandler(_NullHandler())
+ def get_logger(): """ Provides the stem logger. @@ -93,6 +95,7 @@ def get_logger():
return LOGGER
+ def logging_level(runlevel): """ Translates a runlevel into the value expected by the logging module. @@ -105,6 +108,7 @@ def logging_level(runlevel): else: return logging.FATAL + 5
+ def escape(message): """ Escapes specific sequences for logging (newlines, tabs, carriage returns). @@ -119,6 +123,7 @@ def escape(message):
return message
+ def log(runlevel, message): """ Logs a message at the given runlevel. @@ -130,6 +135,7 @@ def log(runlevel, message): if runlevel: LOGGER.log(LOG_VALUES[runlevel], message)
+ def log_once(message_id, runlevel, message): """ Logs a message at the given runlevel. If a message with this ID has already @@ -150,24 +156,31 @@ def log_once(message_id, runlevel, message):
# shorter aliases for logging at a runlevel
+ def trace(message): log(Runlevel.TRACE, message)
+ def debug(message): log(Runlevel.DEBUG, message)
+ def info(message): log(Runlevel.INFO, message)
+ def notice(message): log(Runlevel.NOTICE, message)
+ def warn(message): log(Runlevel.WARN, message)
+ def error(message): log(Runlevel.ERROR, message)
+ class LogBuffer(logging.Handler): """ Basic log handler that listens for stem events and stores them so they can be @@ -198,6 +211,7 @@ class LogBuffer(logging.Handler): def emit(self, record): self._buffer.append(record)
+ class _StdoutLogger(logging.Handler): def __init__(self, runlevel): logging.Handler.__init__(self, level = logging_level(runlevel)) @@ -209,6 +223,7 @@ class _StdoutLogger(logging.Handler): def emit(self, record): print self.formatter.format(record)
+ def log_to_stdout(runlevel): """ Logs further events to stdout. diff --git a/stem/util/ordereddict.py b/stem/util/ordereddict.py index 778a534..b03ffb6 100644 --- a/stem/util/ordereddict.py +++ b/stem/util/ordereddict.py @@ -25,6 +25,7 @@
from UserDict import DictMixin
+ class OrderedDict(dict, DictMixin): def __init__(self, *args, **kwds): if len(args) > 1: diff --git a/stem/util/proc.py b/stem/util/proc.py index 436e061..e5be48d 100644 --- a/stem/util/proc.py +++ b/stem/util/proc.py @@ -62,6 +62,7 @@ Stat = stem.util.enum.Enum( ("CPU_STIME", "stime"), ("START_TIME", "start time") )
+ def is_available(): """ Checks if proc information is available on this platform. @@ -88,6 +89,7 @@ def is_available():
return IS_PROC_AVAILABLE
+ def get_system_start_time(): """ Provides the unix time (seconds since epoch) when the system started. @@ -112,6 +114,7 @@ def get_system_start_time():
return SYS_START_TIME
+ def get_physical_memory(): """ Provides the total physical memory on the system in bytes. @@ -136,6 +139,7 @@ def get_physical_memory():
return SYS_PHYSICAL_MEMORY
+ def get_cwd(pid): """ Provides the current working directory for the given process. @@ -163,6 +167,7 @@ def get_cwd(pid): _log_runtime(parameter, proc_cwd_link, start_time) return cwd
+ def get_uid(pid): """ Provides the user ID the given process is running under. @@ -187,6 +192,7 @@ def get_uid(pid): _log_failure(parameter, exc) raise exc
+ def get_memory_usage(pid): """ Provides the memory usage in bytes for the given process. @@ -219,6 +225,7 @@ def get_memory_usage(pid): _log_failure(parameter, exc) raise exc
+ def get_stats(pid, *stat_types): """ Provides process specific information. See the :data:`~stem.util.proc.Stat` @@ -286,6 +293,7 @@ def get_stats(pid, *stat_types): _log_runtime(parameter, stat_path, start_time) return tuple(results)
+ def get_connections(pid): """ Queries connection related information from the proc contents. This provides @@ -358,6 +366,7 @@ def get_connections(pid): _log_runtime(parameter, "/proc/net/[tcp|udp]", start_time) return conn
+ def _decode_proc_address_encoding(addr): """ Translates an address entry in the /proc/net/* contents to a human readable @@ -396,6 +405,7 @@ def _decode_proc_address_encoding(addr):
return (ip, port)
+ def _is_float(*value): try: for v in value: @@ -405,9 +415,11 @@ def _is_float(*value): except ValueError: return False
+ def _get_line(file_path, line_prefix, parameter): return _get_lines(file_path, (line_prefix, ), parameter)[line_prefix]
+ def _get_lines(file_path, line_prefixes, parameter): """ Fetches lines with the given prefixes from a file. This only provides back @@ -451,6 +463,7 @@ def _get_lines(file_path, line_prefixes, parameter): _log_failure(parameter, exc) raise exc
+ def _log_runtime(parameter, proc_location, start_time): """ Logs a message indicating a successful proc query. @@ -463,6 +476,7 @@ def _log_runtime(parameter, proc_location, start_time): runtime = time.time() - start_time log.debug("proc call (%s): %s (runtime: %0.4f)" % (parameter, proc_location, runtime))
+ def _log_failure(parameter, exc): """ Logs a message indicating that the proc query failed. diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py index 443bd4f..8527f2a 100644 --- a/stem/util/str_tools.py +++ b/stem/util/str_tools.py @@ -44,6 +44,7 @@ TIME_UNITS = ( (1.0, "s", " second"), )
+ def to_camel_case(label, divider = "_", joiner = " "): """ Converts the given string to camel case, ie: @@ -71,6 +72,7 @@ def to_camel_case(label, divider = "_", joiner = " "):
return joiner.join(words)
+ def get_size_label(byte_count, decimal = 0, is_long = False, is_bytes = True): """ Converts a number of bytes into a human readable label in its most @@ -103,6 +105,7 @@ def get_size_label(byte_count, decimal = 0, is_long = False, is_bytes = True): else: return _get_label(SIZE_UNITS_BITS, byte_count, decimal, is_long)
+ def get_time_label(seconds, decimal = 0, is_long = False): """ Converts seconds into a time label truncated to its most significant units. @@ -133,6 +136,7 @@ def get_time_label(seconds, decimal = 0, is_long = False):
return _get_label(TIME_UNITS, seconds, decimal, is_long)
+ def get_time_labels(seconds, is_long = False): """ Provides a list of label conversions for each time unit, starting with its @@ -162,6 +166,7 @@ def get_time_labels(seconds, is_long = False):
return time_labels
+ def get_short_time_label(seconds): """ Provides a time in the following format: @@ -201,6 +206,7 @@ def get_short_time_label(seconds):
return label
+ def parse_short_time_label(label): """ Provides the number of seconds corresponding to the formatting used for the @@ -245,6 +251,7 @@ 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... @@ -277,6 +284,7 @@ def parse_iso_timestamp(entry): 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/system.py b/stem/util/system.py index acba9c1..760cc05 100644 --- a/stem/util/system.py +++ b/stem/util/system.py @@ -59,6 +59,7 @@ GET_CWD_PWDX = "pwdx %s" GET_CWD_LSOF = "lsof -a -p %s -d cwd -Fn" GET_BSD_JAIL_ID_PS = "ps -p %s -o jid"
+ def is_windows(): """ Checks if we are running on Windows. @@ -68,6 +69,7 @@ def is_windows():
return platform.system() == "Windows"
+ def is_mac(): """ Checks if we are running on Mac OSX. @@ -77,6 +79,7 @@ def is_mac():
return platform.system() == "Darwin"
+ def is_bsd(): """ Checks if we are within the BSD family of operating systems. This presently @@ -87,6 +90,7 @@ def is_bsd():
return platform.system() in ("Darwin", "FreeBSD", "OpenBSD")
+ def is_available(command, cached=True): """ Checks the current PATH to see if a command is available or not. If more @@ -128,6 +132,7 @@ def is_available(command, cached=True): CMD_AVAILABLE_CACHE[command] = cmd_exists return cmd_exists
+ def is_running(command): """ Checks for if a process with a given name is running or not. @@ -169,6 +174,7 @@ def is_running(command):
return None
+ def get_pid_by_name(process_name): """ Attempts to determine the process id for a running process, using... @@ -288,6 +294,7 @@ def get_pid_by_name(process_name): log.debug("failed to resolve a pid for '%s'" % process_name) return None
+ def get_pid_by_port(port): """ Attempts to determine the process id for a process with the given port, @@ -411,6 +418,7 @@ def get_pid_by_port(port):
return None # all queries failed
+ def get_pid_by_open_file(path): """ Attempts to determine the process id for a process with the given open file, @@ -448,6 +456,7 @@ def get_pid_by_open_file(path):
return None # all queries failed
+ def get_cwd(pid): """ Provides the working directory of the given process. @@ -515,6 +524,7 @@ def get_cwd(pid):
return None # all queries failed
+ def get_bsd_jail_id(pid): """ Gets the jail id for a process. These seem to only exist for FreeBSD (this @@ -549,6 +559,7 @@ def get_bsd_jail_id(pid):
return 0
+ def expand_path(path, cwd = None): """ Provides an absolute path, expanding tildes with the user's home and @@ -593,6 +604,7 @@ def expand_path(path, cwd = None):
return relative_path
+ def call(command, default = UNDEFINED): """ Issues a command in a subprocess, blocking until completion and returning the diff --git a/stem/util/term.py b/stem/util/term.py index d77fccf..dd2a8d2 100644 --- a/stem/util/term.py +++ b/stem/util/term.py @@ -55,6 +55,7 @@ ATTR_ENCODING = {Attr.BOLD: "1", Attr.UNDERLINE: "4", Attr.HILIGHT: "7"} CSI = "\x1B[%sm" RESET = CSI % "0"
+ def format(msg, *attr): """ Simple terminal text formatting using `ANSI escape sequences diff --git a/stem/util/tor_tools.py b/stem/util/tor_tools.py index 3f15eb2..59a3a59 100644 --- a/stem/util/tor_tools.py +++ b/stem/util/tor_tools.py @@ -32,6 +32,7 @@ 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): """ Checks if a string is a properly formatted relay fingerprint. This checks for @@ -54,6 +55,7 @@ def is_valid_fingerprint(entry, check_prefix = False):
return bool(FINGERPRINT_PATTERN.match(entry))
+ def is_valid_nickname(entry): """ Checks if a string is a valid format for being a nickname. @@ -68,6 +70,7 @@ 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. @@ -80,6 +83,7 @@ def is_valid_circuit_id(entry):
return bool(CIRC_ID_PATTERN.match(entry))
+ def is_valid_stream_id(entry): """ Checks if a string is a valid format for being a stream identifier. @@ -90,6 +94,7 @@ def is_valid_stream_id(entry):
return is_valid_circuit_id(entry)
+ def is_hex_digits(entry, count): """ Checks if a string is the given number of hex digits. Digits represented by diff --git a/stem/version.py b/stem/version.py index 76b8a88..bbbd211 100644 --- a/stem/version.py +++ b/stem/version.py @@ -55,6 +55,7 @@ import stem.util.system # cache for the get_system_tor_version function VERSION_CACHE = {}
+ def get_system_tor_version(tor_cmd = "tor"): """ Queries tor for its version. This is os dependent, only working on linux, @@ -94,6 +95,7 @@ def get_system_tor_version(tor_cmd = "tor"):
return VERSION_CACHE[tor_cmd]
+ class Version(object): """ Comparable tor version. These are constructed from strings that conform to @@ -205,6 +207,7 @@ class Version(object): else: return 0
+ class VersionRequirements(object): """ Series of version constraints that can be compared to. For instance, this diff --git a/test/check_whitespace.py b/test/check_whitespace.py index 6bbbea4..63bc1c1 100644 --- a/test/check_whitespace.py +++ b/test/check_whitespace.py @@ -25,6 +25,7 @@ from stem.util import system # if ran directly then run over everything one level up DEFAULT_TARGET = os.path.sep.join(__file__.split(os.path.sep)[:-1])
+ def pep8_issues(base_path = DEFAULT_TARGET): """ Checks for stylistic issues that are an issue according to the parts of PEP8 @@ -62,7 +63,7 @@ def pep8_issues(base_path = DEFAULT_TARGET): # # Someone else can change this if they really care.
- ignored_issues = "E111,E121,W293,E501,E302,E251,E127" + ignored_issues = "E111,E121,W293,E501,E251,E127"
issues = {} pep8_output = system.call("pep8 --ignore %s %s" % (ignored_issues, base_path)) @@ -76,6 +77,7 @@ def pep8_issues(base_path = DEFAULT_TARGET):
return issues
+ def get_issues(base_path = DEFAULT_TARGET): """ Checks python source code in the given directory for whitespace issues. @@ -151,6 +153,7 @@ def get_issues(base_path = DEFAULT_TARGET):
return issues
+ def _get_files_with_suffix(base_path, suffix = ".py"): """ Iterates over files in a given directory, providing filenames with a certain diff --git a/test/integ/connection/authentication.py b/test/integ/connection/authentication.py index e707b49..50bc3c0 100644 --- a/test/integ/connection/authentication.py +++ b/test/integ/connection/authentication.py @@ -28,6 +28,7 @@ INCORRECT_COOKIE_FAIL = "Authentication failed: Authentication cookie did not ma INCORRECT_SAFECOOKIE_FAIL = "Authentication failed: Safe cookie response did not match expected value." INCORRECT_PASSWORD_FAIL = "Authentication failed: Password did not match HashedControlPassword value from configuration"
+ def _can_authenticate(auth_type): """ Checks if a given authentication method can authenticate to our control @@ -56,6 +57,7 @@ def _can_authenticate(auth_type): else: return False
+ def _get_auth_failure_message(auth_type): """ Provides the message that tor will respond with if our current method of @@ -96,6 +98,7 @@ def _get_auth_failure_message(auth_type):
raise ValueError("No methods of authentication. If this is an open socket then auth shouldn't fail.")
+ class TestAuthenticate(unittest.TestCase): def setUp(self): self.cookie_auth_methods = [stem.connection.AuthMethod.COOKIE] diff --git a/test/integ/connection/connect.py b/test/integ/connection/connect.py index 66b793d..e2d3d02 100644 --- a/test/integ/connection/connect.py +++ b/test/integ/connection/connect.py @@ -9,6 +9,7 @@ import unittest import stem.connection import test.runner
+ class TestConnect(unittest.TestCase): def setUp(self): # prevents the function from printing to the real stdout diff --git a/test/integ/control/base_controller.py b/test/integ/control/base_controller.py index 25af63d..2850bd9 100644 --- a/test/integ/control/base_controller.py +++ b/test/integ/control/base_controller.py @@ -14,6 +14,7 @@ import test.runner import stem.socket import stem.util.system
+ class StateObserver(object): """ Simple container for listening to ControlSocket state changes and @@ -34,6 +35,7 @@ class StateObserver(object): self.state = state self.timestamp = timestamp
+ class TestBaseController(unittest.TestCase): def test_connect_repeatedly(self): """ diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index 6d91f50..a237f0a 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -27,6 +27,7 @@ import test.util from stem.control import EventType from stem.version import Requirement
+ class TestController(unittest.TestCase): def test_from_port(self): """ diff --git a/test/integ/descriptor/__init__.py b/test/integ/descriptor/__init__.py index 6553b2d..691d2f6 100644 --- a/test/integ/descriptor/__init__.py +++ b/test/integ/descriptor/__init__.py @@ -8,6 +8,7 @@ import os
DESCRIPTOR_TEST_DATA = os.path.join(os.path.dirname(__file__), "data")
+ def get_resource(filename): """ Provides the path for a file in our descriptor data directory. diff --git a/test/integ/descriptor/extrainfo_descriptor.py b/test/integ/descriptor/extrainfo_descriptor.py index 1293b3c..2ef5521 100644 --- a/test/integ/descriptor/extrainfo_descriptor.py +++ b/test/integ/descriptor/extrainfo_descriptor.py @@ -14,6 +14,7 @@ import test.runner
from stem.descriptor.extrainfo_descriptor import DirResponse
+ class TestExtraInfoDescriptor(unittest.TestCase): def test_metrics_relay_descriptor(self): """ diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py index 862c166..5442d91 100644 --- a/test/integ/descriptor/networkstatus.py +++ b/test/integ/descriptor/networkstatus.py @@ -14,6 +14,7 @@ import stem.descriptor.networkstatus import stem.version import test.integ.descriptor
+ class TestNetworkStatus(unittest.TestCase): def test_cached_consensus(self): """ diff --git a/test/integ/descriptor/reader.py b/test/integ/descriptor/reader.py index ba807f0..ec0d87e 100644 --- a/test/integ/descriptor/reader.py +++ b/test/integ/descriptor/reader.py @@ -27,9 +27,11 @@ DESCRIPTOR_TEST_DATA = os.path.join(my_dir, "data")
TAR_DESCRIPTORS = None
+ def _get_processed_files_path(): return test.runner.get_runner().get_test_dir("descriptor_processed_files")
+ def _make_processed_files_listing(contents): """ Writes the given 'processed file' listing to disk, returning the path where @@ -44,6 +46,7 @@ def _make_processed_files_listing(contents):
return test_listing_path
+ def _get_raw_tar_descriptors(): global TAR_DESCRIPTORS
@@ -71,6 +74,7 @@ def _get_raw_tar_descriptors():
return TAR_DESCRIPTORS
+ class SkipListener: def __init__(self): self.results = [] # (path, exception) tuples that we've received @@ -78,6 +82,7 @@ class SkipListener: def listener(self, path, exception): self.results.append((path, exception))
+ class TestDescriptorReader(unittest.TestCase): def tearDown(self): # cleans up 'processed file' listings that we made diff --git a/test/integ/descriptor/server_descriptor.py b/test/integ/descriptor/server_descriptor.py index b6b8e62..8cf0bd1 100644 --- a/test/integ/descriptor/server_descriptor.py +++ b/test/integ/descriptor/server_descriptor.py @@ -15,6 +15,7 @@ import stem.version import test.integ.descriptor import test.runner
+ class TestServerDescriptor(unittest.TestCase): def test_metrics_descriptor(self): """ diff --git a/test/integ/process.py b/test/integ/process.py index 6629575..0612163 100644 --- a/test/integ/process.py +++ b/test/integ/process.py @@ -20,6 +20,7 @@ from test import mocking
DATA_DIRECTORY = '/tmp/stem_integ'
+ def _kill_process(process): if stem.prereq.is_python_26(): process.kill() @@ -28,6 +29,7 @@ def _kill_process(process):
process.communicate() # block until its definitely gone
+ class TestProcess(unittest.TestCase): def setUp(self): if not os.path.exists(DATA_DIRECTORY): diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py index 623ce48..b309e91 100644 --- a/test/integ/response/protocolinfo.py +++ b/test/integ/response/protocolinfo.py @@ -16,6 +16,7 @@ import test.runner from test import mocking from test.integ.util.system import filter_system_call
+ class TestProtocolInfo(unittest.TestCase): def setUp(self): mocking.mock(stem.util.proc.is_available, mocking.return_false()) diff --git a/test/integ/socket/control_message.py b/test/integ/socket/control_message.py index 5c78f1a..ccceb26 100644 --- a/test/integ/socket/control_message.py +++ b/test/integ/socket/control_message.py @@ -11,6 +11,7 @@ import stem.socket import stem.version import test.runner
+ class TestControlMessage(unittest.TestCase): def test_unestablished_socket(self): """ diff --git a/test/integ/socket/control_socket.py b/test/integ/socket/control_socket.py index 13daaf4..0d6dd36 100644 --- a/test/integ/socket/control_socket.py +++ b/test/integ/socket/control_socket.py @@ -17,6 +17,7 @@ import stem.control import stem.socket import test.runner
+ class TestControlSocket(unittest.TestCase): def test_send_buffered(self): """ diff --git a/test/integ/util/conf.py b/test/integ/util/conf.py index 6fedca1..6eb1cbb 100644 --- a/test/integ/util/conf.py +++ b/test/integ/util/conf.py @@ -45,9 +45,11 @@ what a beautiful day. Why are those arrows", coming my way?!?"""
+ def _get_test_config_path(): return test.runner.get_runner().get_test_dir("integ_test_cfg")
+ def _make_config(contents): """ Writes a test configuration to disk, returning the path where it is located. @@ -62,6 +64,7 @@ def _make_config(contents):
return test_config_path
+ class TestConf(unittest.TestCase): def tearDown(self): # clears the config contents diff --git a/test/integ/util/proc.py b/test/integ/util/proc.py index 557c7a8..2282831 100644 --- a/test/integ/util/proc.py +++ b/test/integ/util/proc.py @@ -12,6 +12,7 @@ import test.runner
from stem.util import proc
+ class TestProc(unittest.TestCase): def test_get_cwd(self): """ diff --git a/test/integ/util/system.py b/test/integ/util/system.py index 0564118..f6601fc 100644 --- a/test/integ/util/system.py +++ b/test/integ/util/system.py @@ -13,6 +13,7 @@ import test.runner
from test import mocking
+ def filter_system_call(prefixes): """ Provides a functor that passes calls on to the stem.util.system.call() @@ -27,6 +28,7 @@ def filter_system_call(prefixes):
return _filter_system_call
+ def _has_port(): """ True if our test runner has a control port, False otherwise. @@ -34,6 +36,7 @@ def _has_port():
return test.runner.Torrc.PORT in test.runner.get_runner().get_options()
+ class TestSystem(unittest.TestCase): is_extra_tor_running = None
diff --git a/test/integ/version.py b/test/integ/version.py index 9542c63..b40537c 100644 --- a/test/integ/version.py +++ b/test/integ/version.py @@ -9,6 +9,7 @@ import stem.prereq import stem.version import test.runner
+ class TestVersion(unittest.TestCase): def test_get_system_tor_version(self): """ diff --git a/test/mocking.py b/test/mocking.py index 1ebe88d..df545dc 100644 --- a/test/mocking.py +++ b/test/mocking.py @@ -192,27 +192,33 @@ NETWORK_STATUS_DOCUMENT_FOOTER = ( ("directory-signature", "%s %s\n%s" % (DOC_SIG.identity, DOC_SIG.key_digest, DOC_SIG.signature)), )
+ def no_op(): def _no_op(*args): pass
return _no_op
+ def return_value(value): def _return_value(*args): return value
return _return_value
+ def return_true(): return return_value(True)
+ def return_false(): return return_value(False)
+ def return_none(): return return_value(None)
+ def return_for_args(args_to_return_value, default = None, is_method = False): """ Returns a value if the arguments to it match something in a given @@ -274,12 +280,14 @@ def return_for_args(args_to_return_value, default = None, is_method = False):
return _return_value
+ def raise_exception(exception): def _raise(*args): raise exception
return _raise
+ def support_with(obj): """ Provides no-op support for the 'with' keyword, adding __enter__ and __exit__ @@ -295,6 +303,7 @@ def support_with(obj): obj.__dict__["__exit__"] = no_op() return obj
+ def mock(target, mock_call, target_module=None): """ Mocks the given function, saving the initial implementation so it can be @@ -330,6 +339,7 @@ def mock(target, mock_call, target_module=None): else: setattr(target_module, target.__name__, mock_call)
+ def mock_method(target_class, method_name, mock_call): """ Mocks the given method in target_class in a similar fashion as mock() @@ -375,6 +385,7 @@ def mock_method(target_class, method_name, mock_call): # mocks the function with this wrapper setattr(target_class, method_name, mock_wrapper)
+ def revert_mocking(): """ Reverts any mocking done by this function. @@ -399,6 +410,7 @@ def revert_mocking():
MOCK_STATE.clear()
+ def get_real_function(function): """ Provides the original, non-mocked implementation for a function or method. @@ -415,6 +427,7 @@ def get_real_function(function): else: return function
+ def get_all_combinations(attr, include_empty = False): """ Provides an iterator for all combinations of a set of attributes. For @@ -451,6 +464,7 @@ def get_all_combinations(attr, include_empty = False): seen.add(item) yield item
+ def get_object(object_class, methods = None): """ Provides a mock instance of an arbitrary class. Its methods are mocked with @@ -482,6 +496,7 @@ def get_object(object_class, methods = None):
return mock_class()
+ def get_message(content, reformat = True): """ Provides a ControlMessage with content modified to be parsable. This makes @@ -504,6 +519,7 @@ def get_message(content, reformat = True):
return stem.socket.recv_message(StringIO.StringIO(content))
+ def get_protocolinfo_response(**attributes): """ Provides a ProtocolInfoResponse, customized with the given attributes. The @@ -523,6 +539,7 @@ def get_protocolinfo_response(**attributes):
return protocolinfo_response
+ def _get_descriptor_content(attr = None, exclude = (), header_template = (), footer_template = ()): """ Constructs a minimal descriptor with the given attributes. The content we @@ -595,6 +612,7 @@ def _get_descriptor_content(attr = None, exclude = (), header_template = (), foo
return "\n".join(header_content + remainder + footer_content)
+ def get_relay_server_descriptor(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -615,6 +633,7 @@ def get_relay_server_descriptor(attr = None, exclude = (), content = False): desc_content = sign_descriptor_content(desc_content) return stem.descriptor.server_descriptor.RelayDescriptor(desc_content, validate = True)
+ def get_bridge_server_descriptor(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -634,6 +653,7 @@ def get_bridge_server_descriptor(attr = None, exclude = (), content = False): else: return stem.descriptor.server_descriptor.BridgeDescriptor(desc_content, validate = True)
+ def get_relay_extrainfo_descriptor(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -653,6 +673,7 @@ def get_relay_extrainfo_descriptor(attr = None, exclude = (), content = False): else: return stem.descriptor.extrainfo_descriptor.RelayExtraInfoDescriptor(desc_content, validate = True)
+ def get_bridge_extrainfo_descriptor(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -672,6 +693,7 @@ def get_bridge_extrainfo_descriptor(attr = None, exclude = (), content = False): else: return stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor(desc_content, validate = True)
+ def get_router_status_entry_v2(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -691,6 +713,7 @@ def get_router_status_entry_v2(attr = None, exclude = (), content = False): else: return stem.descriptor.router_status_entry.RouterStatusEntryV2(desc_content, validate = True)
+ def get_router_status_entry_v3(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -710,6 +733,7 @@ def get_router_status_entry_v3(attr = None, exclude = (), content = False): else: return stem.descriptor.router_status_entry.RouterStatusEntryV3(desc_content, validate = True)
+ def get_router_status_entry_micro_v3(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -729,6 +753,7 @@ def get_router_status_entry_micro_v3(attr = None, exclude = (), content = False) else: return stem.descriptor.router_status_entry.RouterStatusEntryMicroV3(desc_content, validate = True)
+ def get_directory_authority(attr = None, exclude = (), is_vote = False, content = False): """ Provides the descriptor content for... @@ -760,6 +785,7 @@ def get_directory_authority(attr = None, exclude = (), is_vote = False, content else: return stem.descriptor.networkstatus.DirectoryAuthority(desc_content, validate = True, is_vote = is_vote)
+ def get_key_certificate(attr = None, exclude = (), content = False): """ Provides the descriptor content for... @@ -779,6 +805,7 @@ def get_key_certificate(attr = None, exclude = (), content = False): else: return stem.descriptor.networkstatus.KeyCertificate(desc_content, validate = True)
+ def get_network_status_document_v2(attr = None, exclude = (), routers = None, content = False): """ Provides the descriptor content for... @@ -799,6 +826,7 @@ def get_network_status_document_v2(attr = None, exclude = (), routers = None, co else: return stem.descriptor.networkstatus.NetworkStatusDocumentV2(desc_content, validate = True)
+ def get_network_status_document_v3(attr = None, exclude = (), authorities = None, routers = None, content = False): """ Provides the descriptor content for... @@ -850,6 +878,7 @@ def get_network_status_document_v3(attr = None, exclude = (), authorities = None else: return stem.descriptor.networkstatus.NetworkStatusDocumentV3(desc_content, validate = True)
+ def sign_descriptor_content(desc_content): """ Add a valid signature to the supplied descriptor string. diff --git a/test/network.py b/test/network.py index cab998a..74b4832 100644 --- a/test/network.py +++ b/test/network.py @@ -27,9 +27,11 @@ SOCKS5_NOAUTH_RESPONSE = (0x05, 0x00) SOCKS5_CONN_BY_IPV4 = (0x05, 0x01, 0x00, 0x01) SOCKS5_CONN_BY_NAME = (0x05, 0x01, 0x00, 0x03)
+ class ProxyError(Exception): "Base error for proxy issues."
+ class SocksError(ProxyError): """ Exception raised for any problems returned by the SOCKS proxy. @@ -59,6 +61,7 @@ class SocksError(ProxyError): code = self.code return "[%s] %s" % (code, self._ERROR_MESSAGE[code])
+ class Socks(_socket_socket): """ A **socket.socket**-like interface through a SOCKS5 proxy connection. @@ -218,6 +221,7 @@ class Socks(_socket_socket):
raise NotImplementedError
+ class SocksPatch(object): """ Monkey-patch **socket.socket** to use :class:`~test.network.Socks`, instead. diff --git a/test/output.py b/test/output.py index cadf071..0a3f9da 100644 --- a/test/output.py +++ b/test/output.py @@ -36,22 +36,26 @@ LINE_ATTR = { LineType.CONTENT: (term.Color.CYAN,), }
+ def print_line(msg, *attr): if CONFIG["argument.no_color"]: print msg else: print term.format(msg, *attr)
+ def print_noline(msg, *attr): if CONFIG["argument.no_color"]: sys.stdout.write(msg) else: sys.stdout.write(term.format(msg, *attr))
+ def print_divider(msg, is_header = False): attr = HEADER_ATTR if is_header else CATEGORY_ATTR print_line("%s\n%s\n%s\n" % (DIVIDER, msg.center(70), DIVIDER), *attr)
+ def print_logging(logging_buffer): if not logging_buffer.is_empty(): for entry in logging_buffer: @@ -59,6 +63,7 @@ def print_logging(logging_buffer):
+ def print_config(test_config): print_divider("TESTING CONFIG", True) print_line("Test configuration... ", term.Color.BLUE, term.Attr.BOLD) @@ -74,6 +79,7 @@ def print_config(test_config):
+ def apply_filters(testing_output, *filters): """ Gets the tests results, possibly processed through a series of filters. The @@ -111,6 +117,7 @@ def apply_filters(testing_output, *filters):
return "\n".join(results) + "\n"
+ def colorize(line_type, line_content): """ Applies escape sequences so each line is colored according to its type. @@ -121,6 +128,7 @@ def colorize(line_type, line_content): else: return term.format(line_content, *LINE_ATTR[line_type])
+ def strip_module(line_type, line_content): """ Removes the module name from testing output. This information tends to be @@ -134,6 +142,7 @@ def strip_module(line_type, line_content):
return line_content
+ def align_results(line_type, line_content): """ Strips the normal test results, and adds a right aligned variant instead with @@ -168,6 +177,7 @@ def align_results(line_type, line_content): else: return "%-61s[%s]" % (line_content, term.format(new_ending, term.Attr.BOLD))
+ class ErrorTracker(object): """ Stores any failure or error results we've encountered. diff --git a/test/prompt.py b/test/prompt.py index afbd743..2d5219d 100644 --- a/test/prompt.py +++ b/test/prompt.py @@ -27,6 +27,7 @@ CONTROL_PORT = 2779
STOP_CONFIRMATION = "Would you like to stop the tor instance we made? (y/n, default: n): "
+ def print_usage(): """ Provides a welcoming message. @@ -36,6 +37,7 @@ def print_usage(): print "via the 'controller' variable." print
+ def start(): """ Starts up a tor instance that we can attach a controller to. @@ -51,6 +53,7 @@ def start(): stem.process.launch_tor_with_config(config = tor_config, completion_percent = 5) sys.stdout.write(" done\n\n")
+ def stop(prompt = False): """ Stops the tor instance spawned by this module. @@ -69,6 +72,7 @@ def stop(prompt = False):
os.kill(tor_pid, signal.SIGTERM)
+ def is_running(): """ Checks if we're likely running a tor instance spawned by this module. This is @@ -80,6 +84,7 @@ def is_running():
return bool(stem.util.system.get_pid_by_port(CONTROL_PORT))
+ def controller(): """ Provides a Controller for our tor instance. This starts tor if it isn't diff --git a/test/runner.py b/test/runner.py index dbfed24..bba9e78 100644 --- a/test/runner.py +++ b/test/runner.py @@ -100,12 +100,15 @@ Torrc = stem.util.enum.Enum( # (test_instance, test_name) tuples that we've registered as having been ran RAN_TESTS = []
+ class RunnerStopped(Exception): "Raised when we try to use a Runner that doesn't have an active tor instance"
+ class TorInaccessable(Exception): "Raised when information is needed from tor but the instance we have is inaccessible"
+ def skip(test_case, message): """ Skips the test if we can. The capability for skipping tests was added in @@ -119,6 +122,7 @@ def skip(test_case, message): if stem.prereq.is_python_27(): test_case.skipTest(message)
+ def require_control(test_case): """ Skips the test unless tor provides an endpoint for controllers to attach to. @@ -132,6 +136,7 @@ def require_control(test_case): skip(test_case, "(no connection)") return True
+ def require_version(test_case, req_version): """ Skips the test unless we meet the required version. @@ -146,6 +151,7 @@ def require_version(test_case, req_version): skip(test_case, "(requires %s)" % req_version) return True
+ def require_online(test_case): """ Skips the test if we weren't started with the ONLINE target, which indicates @@ -160,6 +166,7 @@ def require_online(test_case): skip(test_case, "(requires online target)") return True
+ def only_run_once(test_case, test_name): """ Skips the test if it has ran before. If it hasn't then flags it as being ran. @@ -178,6 +185,7 @@ def only_run_once(test_case, test_name): else: RAN_TESTS.append((test_case, test_name))
+ def exercise_controller(test_case, controller): """ Checks that we can now use the socket by issuing a 'GETINFO config-file' @@ -199,6 +207,7 @@ def exercise_controller(test_case, controller):
test_case.assertEquals("config-file=%s\nOK" % torrc_path, str(config_file_response))
+ def get_runner(): """ Singleton for the runtime context of integration tests. @@ -213,6 +222,7 @@ def get_runner():
return INTEG_RUNNER
+ class _MockChrootFile(object): """ Wrapper around a file object that strips given content from readline() @@ -227,6 +237,7 @@ class _MockChrootFile(object): def readline(self): return self.wrapped_file.readline().replace(self.strip_text, "")
+ class Runner(object): def __init__(self): self._runner_lock = threading.RLock() diff --git a/test/unit/connection/authentication.py b/test/unit/connection/authentication.py index 0c05f21..05818b4 100644 --- a/test/unit/connection/authentication.py +++ b/test/unit/connection/authentication.py @@ -16,6 +16,7 @@ import stem.connection from stem.util import log from test import mocking
+ class TestAuthenticate(unittest.TestCase): def setUp(self): mocking.mock(stem.connection.get_protocolinfo, mocking.no_op()) diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py index 34b98a5..7cb14b7 100644 --- a/test/unit/control/controller.py +++ b/test/unit/control/controller.py @@ -14,6 +14,7 @@ from stem.control import _parse_circ_path, Controller, EventType from stem.response import events from test import mocking
+ class TestControl(unittest.TestCase): def setUp(self): socket = stem.socket.ControlSocket() diff --git a/test/unit/descriptor/export.py b/test/unit/descriptor/export.py index bd25c6e..12c6e56 100644 --- a/test/unit/descriptor/export.py +++ b/test/unit/descriptor/export.py @@ -11,6 +11,7 @@ import test.runner from stem.descriptor.export import export_csv, export_csv_file from test.mocking import get_relay_server_descriptor, get_bridge_server_descriptor
+ class TestExport(unittest.TestCase): def test_minimal_descriptor(self): """ diff --git a/test/unit/descriptor/extrainfo_descriptor.py b/test/unit/descriptor/extrainfo_descriptor.py index 4ef0276..382aaee 100644 --- a/test/unit/descriptor/extrainfo_descriptor.py +++ b/test/unit/descriptor/extrainfo_descriptor.py @@ -8,6 +8,7 @@ import unittest from stem.descriptor.extrainfo_descriptor import RelayExtraInfoDescriptor, DirResponse, DirStat from test.mocking import get_relay_extrainfo_descriptor, get_bridge_extrainfo_descriptor, CRYPTO_BLOB
+ class TestExtraInfoDescriptor(unittest.TestCase): def test_minimal_extrainfo_descriptor(self): """ diff --git a/test/unit/descriptor/networkstatus/directory_authority.py b/test/unit/descriptor/networkstatus/directory_authority.py index 68e4713..f3d7fe1 100644 --- a/test/unit/descriptor/networkstatus/directory_authority.py +++ b/test/unit/descriptor/networkstatus/directory_authority.py @@ -9,6 +9,7 @@ import test.runner from stem.descriptor.networkstatus import DirectoryAuthority from test.mocking import get_directory_authority, get_key_certificate, AUTHORITY_HEADER
+ class TestDirectoryAuthority(unittest.TestCase): def test_minimal_consensus_authority(self): """ diff --git a/test/unit/descriptor/networkstatus/document_v2.py b/test/unit/descriptor/networkstatus/document_v2.py index 88561e6..9412cf9 100644 --- a/test/unit/descriptor/networkstatus/document_v2.py +++ b/test/unit/descriptor/networkstatus/document_v2.py @@ -7,6 +7,7 @@ import unittest
from test.mocking import get_network_status_document_v2, NETWORK_STATUS_DOCUMENT_HEADER_V2, NETWORK_STATUS_DOCUMENT_FOOTER_V2
+ class TestNetworkStatusDocument(unittest.TestCase): def test_minimal_document(self): """ diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py index 0ab5694..f2d0bf1 100644 --- a/test/unit/descriptor/networkstatus/document_v3.py +++ b/test/unit/descriptor/networkstatus/document_v3.py @@ -33,6 +33,7 @@ from test.mocking import support_with, \ DOC_SIG, \ NETWORK_STATUS_DOCUMENT_FOOTER
+ class TestNetworkStatusDocument(unittest.TestCase): def test_minimal_consensus(self): """ diff --git a/test/unit/descriptor/networkstatus/key_certificate.py b/test/unit/descriptor/networkstatus/key_certificate.py index fdd90c5..c91c9e3 100644 --- a/test/unit/descriptor/networkstatus/key_certificate.py +++ b/test/unit/descriptor/networkstatus/key_certificate.py @@ -12,6 +12,7 @@ from test.mocking import get_key_certificate, \ KEY_CERTIFICATE_HEADER, \ KEY_CERTIFICATE_FOOTER
+ class TestKeyCertificate(unittest.TestCase): def test_minimal(self): """ diff --git a/test/unit/descriptor/reader.py b/test/unit/descriptor/reader.py index 4ae0aff..985c129 100644 --- a/test/unit/descriptor/reader.py +++ b/test/unit/descriptor/reader.py @@ -8,6 +8,7 @@ import unittest import stem.descriptor.reader import test.mocking as mocking
+ class TestDescriptorReader(unittest.TestCase): def tearDown(self): mocking.revert_mocking() diff --git a/test/unit/descriptor/router_status_entry.py b/test/unit/descriptor/router_status_entry.py index 9565702..e35174c 100644 --- a/test/unit/descriptor/router_status_entry.py +++ b/test/unit/descriptor/router_status_entry.py @@ -15,6 +15,7 @@ from test.mocking import get_router_status_entry_v2, \ get_router_status_entry_micro_v3, \ ROUTER_STATUS_ENTRY_V3_HEADER
+ class TestRouterStatusEntry(unittest.TestCase): def test_fingerprint_decoding(self): """ diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py index e3f4e78..bb49c02 100644 --- a/test/unit/descriptor/server_descriptor.py +++ b/test/unit/descriptor/server_descriptor.py @@ -18,6 +18,7 @@ from test.mocking import get_relay_server_descriptor, \ CRYPTO_BLOB, \ sign_descriptor_content
+ class TestServerDescriptor(unittest.TestCase): def test_minimal_relay_descriptor(self): """ diff --git a/test/unit/exit_policy/policy.py b/test/unit/exit_policy/policy.py index dce4cb7..f4cc499 100644 --- a/test/unit/exit_policy/policy.py +++ b/test/unit/exit_policy/policy.py @@ -8,6 +8,7 @@ from stem.exit_policy import ExitPolicy, \ MicroExitPolicy, \ ExitPolicyRule
+ class TestExitPolicy(unittest.TestCase): def test_example(self): # tests the ExitPolicy and MicroExitPolicy pydoc examples diff --git a/test/unit/exit_policy/rule.py b/test/unit/exit_policy/rule.py index 538418d..176ad26 100644 --- a/test/unit/exit_policy/rule.py +++ b/test/unit/exit_policy/rule.py @@ -6,6 +6,7 @@ import unittest
from stem.exit_policy import AddressType, ExitPolicyRule
+ class TestExitPolicyRule(unittest.TestCase): def test_accept_or_reject(self): self.assertTrue(ExitPolicyRule("accept *:*").is_accept) diff --git a/test/unit/response/authchallenge.py b/test/unit/response/authchallenge.py index 0583d04..314b953 100644 --- a/test/unit/response/authchallenge.py +++ b/test/unit/response/authchallenge.py @@ -20,6 +20,7 @@ INVALID_RESPONSE = "250 AUTHCHALLENGE \ SERVERHASH=FOOBARB16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 \ SERVERNONCE=FOOBAR653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C"
+ class TestAuthChallengeResponse(unittest.TestCase): def test_valid_response(self): """ diff --git a/test/unit/response/control_line.py b/test/unit/response/control_line.py index 546a410..993afcb 100644 --- a/test/unit/response/control_line.py +++ b/test/unit/response/control_line.py @@ -14,6 +14,7 @@ PROTOCOLINFO_RESPONSE = ( 'OK', )
+ class TestControlLine(unittest.TestCase): def test_pop_examples(self): """ diff --git a/test/unit/response/control_message.py b/test/unit/response/control_message.py index 4a55c98..8c9ece7 100644 --- a/test/unit/response/control_message.py +++ b/test/unit/response/control_message.py @@ -30,6 +30,7 @@ version -- The current version of Tor. 250 OK """.replace("\n", "\r\n")
+ class TestControlMessage(unittest.TestCase): def test_ok_response(self): """ diff --git a/test/unit/response/events.py b/test/unit/response/events.py index adc055c..4473c2a 100644 --- a/test/unit/response/events.py +++ b/test/unit/response/events.py @@ -308,6 +308,7 @@ def _get_event(content): stem.response.convert("EVENT", controller_event, arrived_at = 25) return controller_event
+ class TestEvents(unittest.TestCase): def test_example(self): """ diff --git a/test/unit/response/getconf.py b/test/unit/response/getconf.py index a072738..171fc8c 100644 --- a/test/unit/response/getconf.py +++ b/test/unit/response/getconf.py @@ -35,6 +35,7 @@ INVALID_RESPONSE = """\ 123-FOO 232 BAR"""
+ class TestGetConfResponse(unittest.TestCase): def test_empty_response(self): """ diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py index 589d7b9..2fa4f6e 100644 --- a/test/unit/response/getinfo.py +++ b/test/unit/response/getinfo.py @@ -49,6 +49,7 @@ DataDirectory /home/atagar/.tor . 250 OK"""
+ class TestGetInfoResponse(unittest.TestCase): def test_empty_response(self): """ diff --git a/test/unit/response/mapaddress.py b/test/unit/response/mapaddress.py index c3e0b18..23faa6b 100644 --- a/test/unit/response/mapaddress.py +++ b/test/unit/response/mapaddress.py @@ -29,6 +29,7 @@ UNRECOGNIZED_KEYS_RESPONSE = "512 syntax error: mapping '2389' is not of expecte
FAILED_RESPONSE = "451 Resource exhausted"
+ class TestMapAddressResponse(unittest.TestCase): def test_single_response(self): """ diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py index 28cd212..0a209f9 100644 --- a/test/unit/response/protocolinfo.py +++ b/test/unit/response/protocolinfo.py @@ -48,6 +48,7 @@ RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1 250-VERSION Tor="0.2.1.30" 250 OK"""
+ class TestProtocolInfoResponse(unittest.TestCase): def test_convert(self): """ diff --git a/test/unit/response/singleline.py b/test/unit/response/singleline.py index e3b68e5..d48dee2 100644 --- a/test/unit/response/singleline.py +++ b/test/unit/response/singleline.py @@ -12,6 +12,7 @@ from test import mocking MULTILINE_RESPONSE = """250-MULTI 250 LINE"""
+ class TestSingleLineResponse(unittest.TestCase): def test_single_line_response(self): message = mocking.get_message("552 NOTOK") diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py index 47e8107..b0a1588 100644 --- a/test/unit/tutorial.py +++ b/test/unit/tutorial.py @@ -8,6 +8,7 @@ import unittest
from test import mocking
+ class TestTutorial(unittest.TestCase): def tearDown(self): mocking.revert_mocking() diff --git a/test/unit/util/conf.py b/test/unit/util/conf.py index 4e70c1d..5c4e094 100644 --- a/test/unit/util/conf.py +++ b/test/unit/util/conf.py @@ -9,6 +9,7 @@ import stem.util.enum
from stem.util.conf import parse_enum, parse_enum_csv
+ class TestConf(unittest.TestCase): def tearDown(self): # clears the config contents diff --git a/test/unit/util/connection.py b/test/unit/util/connection.py index 358ab49..5f4e62d 100644 --- a/test/unit/util/connection.py +++ b/test/unit/util/connection.py @@ -6,6 +6,7 @@ import unittest
import stem.util.connection
+ class TestConnection(unittest.TestCase): def test_is_valid_ip_address(self): """ diff --git a/test/unit/util/enum.py b/test/unit/util/enum.py index 1b33296..2eae439 100644 --- a/test/unit/util/enum.py +++ b/test/unit/util/enum.py @@ -6,6 +6,7 @@ import unittest
import stem.util.enum
+ class TestEnum(unittest.TestCase): def test_enum_examples(self): """ diff --git a/test/unit/util/proc.py b/test/unit/util/proc.py index 85aff2f..ded149f 100644 --- a/test/unit/util/proc.py +++ b/test/unit/util/proc.py @@ -9,6 +9,7 @@ import unittest from stem.util import proc from test import mocking
+ class TestProc(unittest.TestCase): def tearDown(self): mocking.revert_mocking() diff --git a/test/unit/util/str_tools.py b/test/unit/util/str_tools.py index c1c5ee8..6b10ade 100644 --- a/test/unit/util/str_tools.py +++ b/test/unit/util/str_tools.py @@ -7,6 +7,7 @@ import unittest
from stem.util import str_tools
+ class TestStrTools(unittest.TestCase): def test_to_camel_case(self): """ diff --git a/test/unit/util/system.py b/test/unit/util/system.py index e780375..2f1f6e1 100644 --- a/test/unit/util/system.py +++ b/test/unit/util/system.py @@ -56,6 +56,7 @@ GET_PID_BY_PORT_LSOF_RESULTS = [ "tor 1745 atagar 6u IPv4 14229 0t0 TCP 127.0.0.1:9051 (LISTEN)", "apache 329 atagar 6u IPv4 14229 0t0 TCP 127.0.0.1:80 (LISTEN)"]
+ def mock_call(base_cmd, responses): """ Provides mocking for the system module's call function. There are a couple @@ -93,6 +94,7 @@ def mock_call(base_cmd, responses):
return functools.partial(_mock_call, base_cmd, responses)
+ class TestSystem(unittest.TestCase): def setUp(self): mocking.mock(stem.util.proc.is_available, mocking.return_false()) diff --git a/test/unit/util/tor_tools.py b/test/unit/util/tor_tools.py index 59f448b..634a57b 100644 --- a/test/unit/util/tor_tools.py +++ b/test/unit/util/tor_tools.py @@ -6,6 +6,7 @@ import unittest
import stem.util.tor_tools
+ class TestTorTools(unittest.TestCase): def test_is_valid_fingerprint(self): """ diff --git a/test/unit/version.py b/test/unit/version.py index 78a1a49..95aa690 100644 --- a/test/unit/version.py +++ b/test/unit/version.py @@ -15,6 +15,7 @@ TOR_VERSION_OUTPUT = """Mar 22 23:09:37.088 [notice] Tor v0.2.2.35 \ strong anonymity. (Running on Linux i686) Tor version 0.2.2.35 (git-73ff13ab3cc9570d)."""
+ class TestVersion(unittest.TestCase): def tearDown(self): mocking.revert_mocking() diff --git a/test/util.py b/test/util.py index db6f53c..597dca5 100644 --- a/test/util.py +++ b/test/util.py @@ -18,6 +18,7 @@ Accept-Encoding: identity
"""
+ def external_ip(host, port): """ Returns the externally visible IP address when using a SOCKS4a proxy. @@ -51,6 +52,7 @@ def external_ip(host, port): except Exception, exc: return None
+ def negotiate_socks(sock, host, port): """ Negotiate with a socks4a server. Closes the socket and raises an exception on
tor-commits@lists.torproject.org