From d6aadb579c783c711ae72bffb9a64b9a3215c274 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 18 Aug 2015 13:49:18 -0700 Subject: [PATCH 01/15] Moved connection code into Session.connect_target() --- sulley/sessions.py | 65 +++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/sulley/sessions.py b/sulley/sessions.py index 9ba31c8..5829839 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -448,36 +448,12 @@ def error_handler(error, msg, error_target, error_sock=None): continue try: - # establish a connection to the target. - sock = socket.socket(socket.AF_INET, self.proto) - except Exception, e: - error_handler(e, "failed creating socket", target) + sock = self.connect_target(target) + except socket.error, e: + error_handler(e, "socket connection failed", target, sock) continue - if self.bind: - try: - sock.bind(self.bind) - except Exception, e: - error_handler(e, "failed binding on socket", target, sock) - continue - - try: - sock.settimeout(self.timeout) - # Connect is needed only for TCP stream - if self.proto == socket.SOCK_STREAM: - sock.connect((target.host, target.port)) - except Exception, e: - error_handler(e, "failed connecting on socket", target, sock) - continue - # if SSL is requested, then enable it. - if self.ssl: - try: - ssl_sock = ssl.wrap_socket(sock) - sock = httplib.FakeSocket(sock, ssl_sock) - except Exception, e: - error_handler(e, "failed ssl setup", target, sock) - continue # if the user registered a pre-send function, pass it the sock and let it do the deed. try: @@ -543,6 +519,41 @@ def error_handler(error, msg, error_target, error_sock=None): else: raise Exception("No signal.pause() on windows. #Fixme!") + def connect_target(self, target): + # try: + # establish a connection to the target. + sock = socket.socket(socket.AF_INET, self.proto) + # except Exception, e: + # error_handler(e, "failed creating socket", target) + # continue + + if self.bind: + # try: + sock.bind(self.bind) + # except Exception, e: + # error_handler(e, "failed binding on socket", target, sock) + # continue + + # try: + sock.settimeout(self.timeout) + # Connect is needed only for TCP stream + if self.proto == socket.SOCK_STREAM: + sock.connect((target.host, target.port)) + # except Exception, e: + # error_handler(e, "failed connecting on socket", target, sock) + # continue + + # if SSL is requested, then enable it. + if self.ssl: + # try: + ssl_sock = ssl.wrap_socket(sock) + sock = httplib.FakeSocket(sock, ssl_sock) + # except Exception, e: + # error_handler(e, "failed ssl setup", target, sock) + # continue + + return sock + def import_file(self): """ Load various object values from disk. From e6581d4b24c467c8d9411dd5f3a4bd27cf120ec7 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 18 Aug 2015 16:27:23 -0700 Subject: [PATCH 02/15] Moved connection logic into Target class. --- sulley/sessions.py | 198 +++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 97 deletions(-) diff --git a/sulley/sessions.py b/sulley/sessions.py index 5829839..50898b1 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -26,16 +26,44 @@ class Target(object): Target descriptor container. """ - def __init__(self, host, port): + def __init__(self, host, port, bind=None, proto="tcp", timeout=5.0, logger=None): """ @type host: str @param host: Hostname or IP address of target system @type port: int @param port: Port of target service + @type proto: str + @kwarg proto: (Optional, def="tcp") Communication protocol ("tcp", "udp", "ssl") + @type bind: tuple (host, port) + @kwarg bind: (Optional, def=random) Socket bind address and port + @type timeout: float + @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out + @type logger: logging.RootLogger + @param logger: For debug/info messages, etc. """ + self.max_udp = get_max_udp_size() self.host = host self.port = port + self.bind = bind + self.ssl = False + self.timeout = timeout + self.proto = proto.lower() + self._sock = None + self.logger = logger + + if self.proto == "tcp": + self.proto = socket.SOCK_STREAM + + elif self.proto == "ssl": + self.proto = socket.SOCK_STREAM + self.ssl = True + + elif self.proto == "udp": + self.proto = socket.SOCK_DGRAM + + else: + raise sex.SullyRuntimeError("INVALID PROTOCOL SPECIFIED: %s" % self.proto) # set these manually once target is instantiated. self.netmon = None @@ -45,6 +73,54 @@ def __init__(self, host, port): self.procmon_options = {} self.vmcontrol_options = {} + def open(self): + self._sock = socket.socket(socket.AF_INET, self.proto) + + if self.bind: + self._sock.bind(self.bind) + + self._sock.settimeout(self.timeout) + + # Connect is needed only for TCP stream + if self.proto == socket.SOCK_STREAM: + self._sock.connect((self.host, self.port)) + + # if SSL is requested, then enable it. + if self.ssl: + ssl_sock = ssl.wrap_socket(self._sock) + self._sock = httplib.FakeSocket(self._sock, ssl_sock) + + def send(self, data): + # TCP/SSL + if self.proto == socket.SOCK_STREAM: + self._sock.send(data) + # UDP + elif self.proto == socket.SOCK_DGRAM: + # TODO: this logic does not prevent duplicate test cases, need to address this in the future. + # If our data is over the max UDP size for this platform, truncate before sending + if len(data) > self.max_udp: + self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) + data = data[:self.max_udp] + + self._sock.sendto(data, (self.host, self.port)) + + self.logger.debug("Packet sent : " + repr(data)) + + def close(self): + self._sock.close() + + def recv(self, max_bytes): + """ + Receive up to max_bytes data. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + print(self._sock.proto) + return self._sock.recv(max_bytes) + def pedrpc_connect(self): """ Pass specified target parameters to the PED-RPC server. @@ -109,8 +185,8 @@ def callback(session, node, edge, sock) class Session(pgraph.Graph): def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logging.INFO, logfile=None, - logfile_level=logging.DEBUG, proto="tcp", bind=None, restart_interval=0, timeout=5.0, web_port=26000, - crash_threshold=3, restart_sleep_time=300): + logfile_level=logging.DEBUG, restart_interval=0, web_port=26000, crash_threshold=3, + restart_sleep_time=300): """ Extends pgraph.graph and provides a container for architecting protocol dialogs. @@ -126,12 +202,6 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg @kwarg logfile: (Optional, def=None) Name of log file @type logfile_level: int @kwarg logfile_level: (Optional, def=logger.INFO) Set the log level for the logfile - @type proto: str - @kwarg proto: (Optional, def="tcp") Communication protocol ("tcp", "udp", "ssl") - @type bind: tuple (host, port) - @kwarg bind: (Optional, def=random) Socket bind address and port - @type timeout: float - @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out @type restart_interval: int @kwarg restart_interval (Optional, def=0) Restart the target after n test cases, disable by setting to 0 @type crash_threshold: int @@ -144,7 +214,6 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg super(Session, self).__init__() - self.max_udp = get_max_udp_size() try: import signal @@ -156,11 +225,7 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg self.session_filename = session_filename self.skip = skip self.sleep_time = sleep_time - self.proto = proto.lower() - self.bind = bind - self.ssl = False self.restart_interval = restart_interval - self.timeout = timeout self.web_port = web_port self.crash_threshold = crash_threshold self.restart_sleep_time = restart_sleep_time @@ -192,19 +257,6 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg self.is_paused = False self.crashing_primitives = {} - if self.proto == "tcp": - self.proto = socket.SOCK_STREAM - - elif self.proto == "ssl": - self.proto = socket.SOCK_STREAM - self.ssl = True - - elif self.proto == "udp": - self.proto = socket.SOCK_DGRAM - - else: - raise sex.SullyRuntimeError("INVALID PROTOCOL SPECIFIED: %s" % self.proto) - # import settings if they exist. self.import_file() @@ -244,6 +296,7 @@ def add_target(self, target): # pass specified target parameters to the PED-RPC server. target.pedrpc_connect() + target.logger = self.logger # add target to internal list. self.targets.append(target) @@ -325,9 +378,7 @@ def export_file(self): "skip": self.total_mutant_index, "sleep_time": self.sleep_time, "restart_sleep_time": self.restart_sleep_time, - "proto": self.proto, "restart_interval": self.restart_interval, - "timeout": self.timeout, "web_port": self.web_port, "crash_threshold": self.crash_threshold, "total_num_mutations": self.total_num_mutations, @@ -448,35 +499,33 @@ def error_handler(error, msg, error_target, error_sock=None): continue try: - sock = self.connect_target(target) + target.open() except socket.error, e: - error_handler(e, "socket connection failed", target, sock) + error_handler(e, "socket connection failed", target, target) continue - - # if the user registered a pre-send function, pass it the sock and let it do the deed. try: - self.pre_send(sock) + self.pre_send(target) except Exception, e: - error_handler(e, "pre_send() failed", target, sock) + error_handler(e, "pre_send() failed", target, target) continue # send out valid requests for each node in the current path up to the node we are fuzzing. try: for e in path[:-1]: node = self.nodes[e.dst] - self.transmit(sock, node, e) + self.transmit(target, node, e) except Exception, e: - error_handler(e, "failed transmitting a node up the path", target, sock) + error_handler(e, "failed transmitting a node up the path", target, target) continue # now send the current node we are fuzzing. try: - self.transmit(sock, self.fuzz_node, edge) + self.transmit(target, self.fuzz_node, edge) except Exception, e: - error_handler(e, "failed transmitting fuzz node", target, sock) - continue + error_handler(e, "failed transmitting fuzz node", target, target) + raise # if we reach this point the send was successful for break out of the while(1). break @@ -485,12 +534,12 @@ def error_handler(error, msg, error_target, error_sock=None): # We do this outside the try/except loop because if our fuzz causes a crash then the post_send() # will likely fail and we don't want to sit in an endless loop. try: - self.post_send(sock) + self.post_send(target) except Exception, e: - error_handler(e, "post_send() failed", target, sock) + error_handler(e, "post_send() failed", target, target) # done with the socket. - sock.close() + target.close() # delay in between test cases. self.logger.info("sleeping for %f seconds" % self.sleep_time) @@ -519,41 +568,6 @@ def error_handler(error, msg, error_target, error_sock=None): else: raise Exception("No signal.pause() on windows. #Fixme!") - def connect_target(self, target): - # try: - # establish a connection to the target. - sock = socket.socket(socket.AF_INET, self.proto) - # except Exception, e: - # error_handler(e, "failed creating socket", target) - # continue - - if self.bind: - # try: - sock.bind(self.bind) - # except Exception, e: - # error_handler(e, "failed binding on socket", target, sock) - # continue - - # try: - sock.settimeout(self.timeout) - # Connect is needed only for TCP stream - if self.proto == socket.SOCK_STREAM: - sock.connect((target.host, target.port)) - # except Exception, e: - # error_handler(e, "failed connecting on socket", target, sock) - # continue - - # if SSL is requested, then enable it. - if self.ssl: - # try: - ssl_sock = ssl.wrap_socket(sock) - sock = httplib.FakeSocket(sock, ssl_sock) - # except Exception, e: - # error_handler(e, "failed ssl setup", target, sock) - # continue - - return sock - def import_file(self): """ Load various object values from disk. @@ -572,9 +586,7 @@ def import_file(self): self.session_filename = data["session_filename"] self.sleep_time = data["sleep_time"] self.restart_sleep_time = data["restart_sleep_time"] - self.proto = data["proto"] self.restart_interval = data["restart_interval"] - self.timeout = data["timeout"] self.web_port = data["web_port"] self.crash_threshold = data["crash_threshold"] self.total_num_mutations = data["total_num_mutations"] @@ -815,27 +827,19 @@ def transmit(self, sock, node, edge): # Try to send payload down-range try: - # TCP/SSL - if self.proto == socket.SOCK_STREAM: - sock.send(data) - # UDP - elif self.proto == socket.SOCK_DGRAM: - # TODO: this logic does not prevent duplicate test cases, need to address this in the future. - # If our data is over the max UDP size for this platform, truncate before sending - if len(data) > self.max_udp: - self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) - data = data[:self.max_udp] - - sock.sendto(data, (self.targets[0].host, self.targets[0].port)) - - self.logger.debug("Packet sent : " + repr(data)) + self.targets[0].send(data) + except socket.error, inst: + self.logger.error("Socket error on send: %s" % inst) + raise + try: # Receive data # TODO: Remove magic number (10000) - self.last_recv = sock.recv(10000) + self.last_recv = self.targets[0].recv(10000) + except socket.error, inst: + self.logger.error("Socket error on receive: %s" % inst) + raise - except Exception, inst: - self.logger.error("Socket error, send: %s" % inst) # If we have data in our recv buffer if self.last_recv: From 09628fb72c66e1fb3be2ed484a6c10374afd41af Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 18 Aug 2015 16:35:38 -0700 Subject: [PATCH 03/15] Improved comments. --- sulley/sessions.py | 88 ++++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/sulley/sessions.py b/sulley/sessions.py index 50898b1..95c21f3 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -24,9 +24,12 @@ class Target(object): """ Target descriptor container. + Encapsulates connection logic for the target, as well as pedrpc connection logic. + + Contains a logger which is configured by Session.add_target(). """ - def __init__(self, host, port, bind=None, proto="tcp", timeout=5.0, logger=None): + def __init__(self, host, port, proto="tcp", bind=None, timeout=5.0): """ @type host: str @param host: Hostname or IP address of target system @@ -38,8 +41,6 @@ def __init__(self, host, port, bind=None, proto="tcp", timeout=5.0, logger=None) @kwarg bind: (Optional, def=random) Socket bind address and port @type timeout: float @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out - @type logger: logging.RootLogger - @param logger: For debug/info messages, etc. """ self.max_udp = get_max_udp_size() @@ -50,7 +51,7 @@ def __init__(self, host, port, bind=None, proto="tcp", timeout=5.0, logger=None) self.timeout = timeout self.proto = proto.lower() self._sock = None - self.logger = logger + self.logger = None if self.proto == "tcp": self.proto = socket.SOCK_STREAM @@ -73,7 +74,20 @@ def __init__(self, host, port, bind=None, proto="tcp", timeout=5.0, logger=None) self.procmon_options = {} self.vmcontrol_options = {} + def close(self): + """ + Close connection to the target. + + :return: None + """ + self._sock.close() + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ self._sock = socket.socket(socket.AF_INET, self.proto) if self.bind: @@ -90,37 +104,6 @@ def open(self): ssl_sock = ssl.wrap_socket(self._sock) self._sock = httplib.FakeSocket(self._sock, ssl_sock) - def send(self, data): - # TCP/SSL - if self.proto == socket.SOCK_STREAM: - self._sock.send(data) - # UDP - elif self.proto == socket.SOCK_DGRAM: - # TODO: this logic does not prevent duplicate test cases, need to address this in the future. - # If our data is over the max UDP size for this platform, truncate before sending - if len(data) > self.max_udp: - self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) - data = data[:self.max_udp] - - self._sock.sendto(data, (self.host, self.port)) - - self.logger.debug("Packet sent : " + repr(data)) - - def close(self): - self._sock.close() - - def recv(self, max_bytes): - """ - Receive up to max_bytes data. - - :param max_bytes: Maximum number of bytes to receive. - :type max_bytes: int - - :return: Received data. - """ - print(self._sock.proto) - return self._sock.recv(max_bytes) - def pedrpc_connect(self): """ Pass specified target parameters to the PED-RPC server. @@ -155,6 +138,41 @@ def pedrpc_connect(self): for key in self.netmon_options.keys(): eval('self.netmon.set_%s(self.netmon_options["%s"])' % (key, key)) + def recv(self, max_bytes): + """ + Receive up to max_bytes data from the target. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + print(self._sock.proto) + return self._sock.recv(max_bytes) + + def send(self, data): + """ + Send data to the target. Only valid after calling open! + + :param data: Data to send. + + :return: None + """ + # TCP/SSL + if self.proto == socket.SOCK_STREAM: + self._sock.send(data) + # UDP + elif self.proto == socket.SOCK_DGRAM: + # TODO: this logic does not prevent duplicate test cases, need to address this in the future. + # If our data is over the max UDP size for this platform, truncate before sending + if len(data) > self.max_udp: + self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) + data = data[:self.max_udp] + + self._sock.sendto(data, (self.host, self.port)) + + self.logger.debug("Packet sent : " + repr(data)) + class Connection(pgraph.Edge): def __init__(self, src, dst, callback=None): From 58a4f52b0caf50b3b2ecdd269fd638345334e67d Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 18 Aug 2015 17:01:33 -0700 Subject: [PATCH 04/15] Target.recv() now handles socket.timeout itself and returns bytes(''). --- sulley/sessions.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sulley/sessions.py b/sulley/sessions.py index 95c21f3..7cfbdc5 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -147,8 +147,10 @@ def recv(self, max_bytes): :return: Received data. """ - print(self._sock.proto) - return self._sock.recv(max_bytes) + try: + return self._sock.recv(max_bytes) + except socket.timeout: + return bytes('') def send(self, data): """ @@ -848,7 +850,6 @@ def transmit(self, sock, node, edge): self.targets[0].send(data) except socket.error, inst: self.logger.error("Socket error on send: %s" % inst) - raise try: # Receive data @@ -856,15 +857,13 @@ def transmit(self, sock, node, edge): self.last_recv = self.targets[0].recv(10000) except socket.error, inst: self.logger.error("Socket error on receive: %s" % inst) - raise - # If we have data in our recv buffer if self.last_recv: self.logger.debug("received: [%d] %s" % (len(self.last_recv), repr(self.last_recv))) # Assume a crash? else: - self.logger.warning("Nothing received on socket.") + self.logger.warning("Nothing received from target.") # Increment individual crash count self.crashing_primitives[self.fuzz_node.mutant] = self.crashing_primitives.get(self.fuzz_node.mutant, 0) + 1 # Note crash information From 6bf179f152770e562fa33f1e1d757880f2511111 Mon Sep 17 00:00:00 2001 From: josh Date: Wed, 19 Aug 2015 11:41:26 -0700 Subject: [PATCH 05/15] Extracted connection functionality into SocketConnection. Added ITargetConnection to describe interface. --- sulley/itarget_connection.py | 51 ++++++++++++++ sulley/sessions.py | 78 ++++++--------------- sulley/socket_connection.py | 127 +++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 59 deletions(-) create mode 100644 sulley/itarget_connection.py create mode 100644 sulley/socket_connection.py diff --git a/sulley/itarget_connection.py b/sulley/itarget_connection.py new file mode 100644 index 0000000..e10b0fd --- /dev/null +++ b/sulley/itarget_connection.py @@ -0,0 +1,51 @@ +import abc + + +class ITargetConnection(object): + """ + Interface for connections to fuzzing targets. + Target connections may be opened and closed multiple times. You must open before using send/recv and close + afterwards. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def close(self): + """ + Close connection. + + :return: None + """ + raise NotImplementedError + + @abc.abstractmethod + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + raise NotImplementedError + + @abc.abstractmethod + def recv(self, max_bytes): + """ + Receive up to max_bytes data. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. bytes('') if no data is received. + """ + raise NotImplementedError + + @abc.abstractmethod + def send(self, data): + """ + Send data to the target. + + :param data: Data to send. + + :return: None + """ + raise NotImplementedError diff --git a/sulley/sessions.py b/sulley/sessions.py index 7cfbdc5..9bb7381 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -2,18 +2,15 @@ import zlib import time import socket -import ssl import signal import cPickle import threading -import httplib import logging import blocks import pgraph import sex import primitives - -from helpers import get_max_udp_size +import socket_connection from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer @@ -42,29 +39,10 @@ def __init__(self, host, port, proto="tcp", bind=None, timeout=5.0): @type timeout: float @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out """ - self.max_udp = get_max_udp_size() - - self.host = host - self.port = port - self.bind = bind - self.ssl = False - self.timeout = timeout - self.proto = proto.lower() - self._sock = None self.logger = None - if self.proto == "tcp": - self.proto = socket.SOCK_STREAM - - elif self.proto == "ssl": - self.proto = socket.SOCK_STREAM - self.ssl = True - - elif self.proto == "udp": - self.proto = socket.SOCK_DGRAM - - else: - raise sex.SullyRuntimeError("INVALID PROTOCOL SPECIFIED: %s" % self.proto) + self.target_connection = socket_connection.SocketConnection( + host=host, port=port, proto=proto, bind=bind, timeout=timeout) # set these manually once target is instantiated. self.netmon = None @@ -80,7 +58,7 @@ def close(self): :return: None """ - self._sock.close() + self.target_connection.close() def open(self): """ @@ -88,21 +66,7 @@ def open(self): :return: None """ - self._sock = socket.socket(socket.AF_INET, self.proto) - - if self.bind: - self._sock.bind(self.bind) - - self._sock.settimeout(self.timeout) - - # Connect is needed only for TCP stream - if self.proto == socket.SOCK_STREAM: - self._sock.connect((self.host, self.port)) - - # if SSL is requested, then enable it. - if self.ssl: - ssl_sock = ssl.wrap_socket(self._sock) - self._sock = httplib.FakeSocket(self._sock, ssl_sock) + self.target_connection.open() def pedrpc_connect(self): """ @@ -147,10 +111,7 @@ def recv(self, max_bytes): :return: Received data. """ - try: - return self._sock.recv(max_bytes) - except socket.timeout: - return bytes('') + return self.target_connection.recv(max_bytes=max_bytes) def send(self, data): """ @@ -160,20 +121,19 @@ def send(self, data): :return: None """ - # TCP/SSL - if self.proto == socket.SOCK_STREAM: - self._sock.send(data) - # UDP - elif self.proto == socket.SOCK_DGRAM: - # TODO: this logic does not prevent duplicate test cases, need to address this in the future. - # If our data is over the max UDP size for this platform, truncate before sending - if len(data) > self.max_udp: - self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) - data = data[:self.max_udp] + self.target_connection.send(data=data) + + def set_logger(self, logger): + """ + Set this object's (and it's aggregated classes') logger. - self._sock.sendto(data, (self.host, self.port)) + :param logger: Logger to use. + :type logger: logging.Logger - self.logger.debug("Packet sent : " + repr(data)) + :return: None + """ + self.logger = logger + self.target_connection.set_logger(logger=logger) class Connection(pgraph.Edge): @@ -310,13 +270,13 @@ def add_target(self, target): """ Add a target to the session. Multiple targets can be added for parallel fuzzing. - @type target: session.target + @type target: Target @param target: Target to add to session """ # pass specified target parameters to the PED-RPC server. target.pedrpc_connect() - target.logger = self.logger + target.set_logger(logger=self.logger) # add target to internal list. self.targets.append(target) diff --git a/sulley/socket_connection.py b/sulley/socket_connection.py new file mode 100644 index 0000000..32b6fe1 --- /dev/null +++ b/sulley/socket_connection.py @@ -0,0 +1,127 @@ +import itarget_connection +import socket +import ssl +import httplib + +import sex +from helpers import get_max_udp_size + + +class SocketConnection(itarget_connection.ITargetConnection): + """ + ITargetConnection implementation using sockets. Supports UDP, TCP, SSL. + """ + def __init__(self, host, port, proto="tcp", bind=None, timeout=5.0): + """ + @type host: str + @param host: Hostname or IP address of target system + @type port: int + @param port: Port of target service + @type proto: str + @kwarg proto: (Optional, def="tcp") Communication protocol ("tcp", "udp", "ssl") + @type bind: tuple (host, port) + @kwarg bind: (Optional, def=random) Socket bind address and port + @type timeout: float + @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out + """ + self.max_udp = get_max_udp_size() + + self.host = host + self.port = port + self.bind = bind + self.ssl = False + self.timeout = timeout + self.proto = proto.lower() + self._sock = None + self.logger = None + + if self.proto == "tcp": + self.proto = socket.SOCK_STREAM + + elif self.proto == "ssl": + self.proto = socket.SOCK_STREAM + self.ssl = True + + elif self.proto == "udp": + self.proto = socket.SOCK_DGRAM + + else: + raise sex.SullyRuntimeError("INVALID PROTOCOL SPECIFIED: %s" % self.proto) + + def close(self): + """ + Close connection to the target. + + :return: None + """ + self._sock.close() + + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + self._sock = socket.socket(socket.AF_INET, self.proto) + + if self.bind: + self._sock.bind(self.bind) + + self._sock.settimeout(self.timeout) + + # Connect is needed only for TCP stream + if self.proto == socket.SOCK_STREAM: + self._sock.connect((self.host, self.port)) + + # if SSL is requested, then enable it. + if self.ssl: + ssl_sock = ssl.wrap_socket(self._sock) + self._sock = httplib.FakeSocket(self._sock, ssl_sock) + + def recv(self, max_bytes): + """ + Receive up to max_bytes data from the target. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + try: + return self._sock.recv(max_bytes) + except socket.timeout: + return bytes('') + + def send(self, data): + """ + Send data to the target. Only valid after calling open! + + :param data: Data to send. + + :return: None + """ + # TCP/SSL + if self.proto == socket.SOCK_STREAM: + self._sock.send(data) + # UDP + elif self.proto == socket.SOCK_DGRAM: + # TODO: this logic does not prevent duplicate test cases, need to address this in the future. + # If our data is over the max UDP size for this platform, truncate before sending + if len(data) > self.max_udp: + self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) + data = data[:self.max_udp] + + self._sock.sendto(data, (self.host, self.port)) + + self.logger.debug("Packet sent : " + repr(data)) + + def set_logger(self, logger): + """ + Set this object's (and it's aggregated classes') logger. + + :param logger: Logger to use. + :type logger: logging.Logger + + :return: None + """ + self.logger = logger From b40165d2ee826f917df90c13684fef3350cfd393 Mon Sep 17 00:00:00 2001 From: josh Date: Wed, 19 Aug 2015 13:50:59 -0700 Subject: [PATCH 06/15] Added serial interface. This is kind of dinky without a logger, so there are print statements in the send and recv methods for now. --- sulley/serial_connection.py | 81 +++++++++++++++++++++++++++++++++++++ sulley/serial_target.py | 30 ++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 sulley/serial_connection.py create mode 100644 sulley/serial_target.py diff --git a/sulley/serial_connection.py b/sulley/serial_connection.py new file mode 100644 index 0000000..5dd345b --- /dev/null +++ b/sulley/serial_connection.py @@ -0,0 +1,81 @@ +import itarget_connection +import serial + + +class SerialConnection(itarget_connection.ITargetConnection): + """ + ITargetConnection implementation using serial ports. + """ + def __init__(self, port, baudrate): + """ + @type port: int | str + @param port: Serial port name or number. + @type baudrate: int + @param baudrate: Baud rate for port. + """ + self._device = None + self.port = port + self.baudrate = baudrate + self.logger = None + + def close(self): + """ + Close connection to the target. + + :return: None + """ + self._device.close() + + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + self._device = serial.Serial(port=self.port, baudrate=self.baudrate) + + def recv(self, max_bytes): + """ + Receive up to max_bytes data from the target. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + + self._device.timeout = 0.010 + + fragment = self._device.read(size=1024) + data = fragment + + # Serial ports can be slow and render only a few bytes at a time. + # Therefore, we keep reading until we get nothing, in hopes of getting a full packet. + while fragment: + fragment = self._device.read(size=1024) + data += fragment + + print("recv:{0}".format(data)) + return data + + def send(self, data): + """ + Send data to the target. Only valid after calling open! + + :param data: Data to send. + + :return: None + """ + print("send:{0}".format(data)) + self._device.write(data) + + def set_logger(self, logger): + """ + Set this object's (and it's aggregated classes') logger. + + :param logger: Logger to use. + :type logger: logging.Logger + + :return: None + """ + self.logger = logger diff --git a/sulley/serial_target.py b/sulley/serial_target.py new file mode 100644 index 0000000..2d0b9c6 --- /dev/null +++ b/sulley/serial_target.py @@ -0,0 +1,30 @@ +import sessions +import serial_connection + + +class SerialTarget(sessions.Target): + """ + Target class that uses a SerailConnection. + Encapsulates connection logic for the target, as well as pedrpc connection logic. + + Contains a logger which is configured by Session.add_target(). + """ + + def __init__(self, port=0, baudrate=9600): + """ + @type port: int | str + @param port: Serial port name or number. + @type baudrate: int + @param baudrate: Baud rate for port. + """ + super(SerialTarget, self).__init__(host="", port=1) + + self.target_connection = serial_connection.SerialConnection(port=port, baudrate=baudrate) + + # set these manually once target is instantiated. + self.netmon = None + self.procmon = None + self.vmcontrol = None + self.netmon_options = {} + self.procmon_options = {} + self.vmcontrol_options = {} From 2c916969f9f4d382ff80655d45804d14b43f2cf5 Mon Sep 17 00:00:00 2001 From: josh Date: Thu, 20 Aug 2015 10:27:15 -0700 Subject: [PATCH 07/15] Added local logging - works with serial. --- sulley/fuzz_logger.py | 79 +++++++++++++++++++++++++++++++++++++ sulley/ifuzz_logger.py | 45 +++++++++++++++++++++ sulley/serial_connection.py | 20 ++++++---- sulley/serial_target.py | 19 +++++---- sulley/sessions.py | 45 ++++++++++++++++----- 5 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 sulley/fuzz_logger.py create mode 100644 sulley/ifuzz_logger.py diff --git a/sulley/fuzz_logger.py b/sulley/fuzz_logger.py new file mode 100644 index 0000000..375ba03 --- /dev/null +++ b/sulley/fuzz_logger.py @@ -0,0 +1,79 @@ +import ifuzz_logger +import os +import errno + + +class FuzzLogger(ifuzz_logger.IFuzzLogger): + """ + IFuzzLogger that saves sent and received data to files within a directory. + + File format is: -(rx|tx)-.txt + """ + + def __init__(self, path): + """ + :param path: Directory in which to save fuzz data. + """ + self._path = path + self._current_id = '' + self._rx_count = 0 + self._tx_count = 0 + + # mkdir -p self._path + try: + os.makedirs(self._path) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + + def open_test_case(self, test_case_id): + """ + Open a test case - i.e., a fuzzing mutation. + + :param test_case_id: Test case name/number. Should be unique. + + :return: None + """ + self._current_id = str(test_case_id) + self._rx_count = 0 + self._tx_count = 0 + + def log_send(self, data): + """ + Records data as about to be sent to the target. + + :param data: Transmitted data + :type data: buffer + + :return: None + :rtype: None + """ + self._tx_count += 1 + + filename = "{0}-tx-{1}.txt".format(self._current_id, self._tx_count) + full_name = os.path.join(self._path, filename) + + # Write data in binary mode to avoid newline conversion + with open(full_name, "wb") as file_handle: + file_handle.write(data) + + def log_recv(self, data): + """ + Records data as having been received from the target. + + :param data: Received data. + :type data: buffer + + :return: None + :rtype: None + """ + self._rx_count += 1 + + filename = "{0}-rx-{1}.txt".format(self._current_id, self._tx_count) + full_name = os.path.join(self._path, filename) + + # Write data in binary mode to avoid newline conversion + with open(full_name, "wb") as file_handle: + file_handle.write(data) diff --git a/sulley/ifuzz_logger.py b/sulley/ifuzz_logger.py new file mode 100644 index 0000000..309c44a --- /dev/null +++ b/sulley/ifuzz_logger.py @@ -0,0 +1,45 @@ +import abc + + +class IFuzzLogger(object): + """ + Abstract class for logging fuzz data. Allows for logging approaches. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def open_test_case(self, test_case_id): + """ + Open a test case - i.e., a fuzzing mutation. + + :param test_case_id: Test case name/number. Should be unique. + + :return: None + """ + raise NotImplementedError + + @abc.abstractmethod + def log_send(self, data): + """ + Records data as about to be sent to the target. + + :param data: Transmitted data + :type data: buffer + + :return: None + :rtype: None + """ + raise NotImplementedError + + @abc.abstractmethod + def log_recv(self, data): + """ + Records data as having been received from the target. + + :param data: Received data. + :type data: buffer + + :return: None + :rtype: None + """ + raise NotImplementedError diff --git a/sulley/serial_connection.py b/sulley/serial_connection.py index 5dd345b..d9521e6 100644 --- a/sulley/serial_connection.py +++ b/sulley/serial_connection.py @@ -5,18 +5,24 @@ class SerialConnection(itarget_connection.ITargetConnection): """ ITargetConnection implementation using serial ports. + + Messages are time-delimited, based on a parameter given to the constructor. """ - def __init__(self, port, baudrate): + + def __init__(self, port, baudrate, message_separator_time=0.300): """ - @type port: int | str - @param port: Serial port name or number. - @type baudrate: int - @param baudrate: Baud rate for port. + @type port: int | str + @param port: Serial port name or number. + @type baudrate: int + @param baudrate: Baud rate for port. + @type message_separator_time: float + @param message_separator_time: The amount of time to wait before considering a reply from the target complete. """ self._device = None self.port = port self.baudrate = baudrate self.logger = None + self.message_separator_time = message_separator_time def close(self): """ @@ -44,7 +50,7 @@ def recv(self, max_bytes): :return: Received data. """ - self._device.timeout = 0.010 + self._device.timeout = self.message_separator_time fragment = self._device.read(size=1024) data = fragment @@ -55,7 +61,6 @@ def recv(self, max_bytes): fragment = self._device.read(size=1024) data += fragment - print("recv:{0}".format(data)) return data def send(self, data): @@ -66,7 +71,6 @@ def send(self, data): :return: None """ - print("send:{0}".format(data)) self._device.write(data) def set_logger(self, logger): diff --git a/sulley/serial_target.py b/sulley/serial_target.py index 2d0b9c6..e8d6d5c 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -4,22 +4,27 @@ class SerialTarget(sessions.Target): """ - Target class that uses a SerailConnection. + Target class that uses a SerailConnection. Serial messages are assumed to be time-separated. Encapsulates connection logic for the target, as well as pedrpc connection logic. Contains a logger which is configured by Session.add_target(). """ - def __init__(self, port=0, baudrate=9600): + def __init__(self, port=0, baudrate=9600, message_separator_time=0.300): """ - @type port: int | str - @param port: Serial port name or number. - @type baudrate: int - @param baudrate: Baud rate for port. + @type port: int | str + @param port: Serial port name or number. + @type baudrate: int + @param baudrate: Baud rate for port. + @type message_separator_time: float + @param message_separator_time: The amount of time to wait before considering a reply from the target complete. """ super(SerialTarget, self).__init__(host="", port=1) - self.target_connection = serial_connection.SerialConnection(port=port, baudrate=baudrate) + self._target_connection = serial_connection.SerialConnection( + port=port, + baudrate=baudrate, + message_separator_time=message_separator_time) # set these manually once target is instantiated. self.netmon = None diff --git a/sulley/sessions.py b/sulley/sessions.py index 9bb7381..60c80c1 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -11,6 +11,7 @@ import sex import primitives import socket_connection +import ifuzz_logger from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer @@ -39,9 +40,10 @@ def __init__(self, host, port, proto="tcp", bind=None, timeout=5.0): @type timeout: float @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out """ - self.logger = None + self._logger = None + self._fuzz_data_logger = None - self.target_connection = socket_connection.SocketConnection( + self._target_connection = socket_connection.SocketConnection( host=host, port=port, proto=proto, bind=bind, timeout=timeout) # set these manually once target is instantiated. @@ -58,7 +60,7 @@ def close(self): :return: None """ - self.target_connection.close() + self._target_connection.close() def open(self): """ @@ -66,7 +68,7 @@ def open(self): :return: None """ - self.target_connection.open() + self._target_connection.open() def pedrpc_connect(self): """ @@ -111,7 +113,12 @@ def recv(self, max_bytes): :return: Received data. """ - return self.target_connection.recv(max_bytes=max_bytes) + data = self._target_connection.recv(max_bytes=max_bytes) + + if self._fuzz_data_logger is not None: + self._fuzz_data_logger.log_recv(data) + + return data def send(self, data): """ @@ -121,7 +128,9 @@ def send(self, data): :return: None """ - self.target_connection.send(data=data) + if self._fuzz_data_logger is not None: + self._fuzz_data_logger.log_send(data) + self._target_connection.send(data=data) def set_logger(self, logger): """ @@ -132,8 +141,19 @@ def set_logger(self, logger): :return: None """ - self.logger = logger - self.target_connection.set_logger(logger=logger) + self._logger = logger + self._target_connection.set_logger(logger=logger) + + def set_fuzz_data_logger(self, fuzz_data_logger): + """ + Set this object's fuzz data logger -- for sent and received fuzz data. + + :param fuzz_data_logger: New logger. + :type fuzz_data_logger: ifuzz_logger.IFuzzLogger + + :return: None + """ + self._fuzz_data_logger = fuzz_data_logger class Connection(pgraph.Edge): @@ -166,7 +186,7 @@ def callback(session, node, edge, sock) class Session(pgraph.Graph): def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logging.INFO, logfile=None, logfile_level=logging.DEBUG, restart_interval=0, web_port=26000, crash_threshold=3, - restart_sleep_time=300): + restart_sleep_time=300, fuzz_data_logger=None): """ Extends pgraph.graph and provides a container for architecting protocol dialogs. @@ -188,8 +208,10 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg @kwarg crash_threshold (Optional, def=3) Maximum number of crashes allowed before a node is exhaust @type restart_sleep_time: int @kwarg restart_sleep_time: Optional, def=300) Time in seconds to sleep when target can't be restarted - @type web_port: int + @type web_port: int @kwarg web_port: (Optional, def=26000) Port for monitoring fuzzing campaign via a web browser + @type fuzz_data_logger: ifuzz_logger.IFuzzLogger + @kwarg fuzz_data_logger: For saving data sent to and from the target. """ super(Session, self).__init__() @@ -209,6 +231,7 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg self.web_port = web_port self.crash_threshold = crash_threshold self.restart_sleep_time = restart_sleep_time + self._fuzz_data_logger = fuzz_data_logger # Initialize logger self.logger = logging.getLogger("Sulley_logger") @@ -277,6 +300,7 @@ def add_target(self, target): # pass specified target parameters to the PED-RPC server. target.pedrpc_connect() target.set_logger(logger=self.logger) + target.set_fuzz_data_logger(fuzz_data_logger=self._fuzz_data_logger) # add target to internal list. self.targets.append(target) @@ -459,6 +483,7 @@ def error_handler(error, msg, error_target, error_sock=None): # if we don't need to skip the current test case. if self.total_mutant_index > self.skip: self.logger.info("fuzzing %d of %d" % (self.fuzz_node.mutant_index, num_mutations)) + self._fuzz_data_logger.open_test_case(self.total_mutant_index) # attempt to complete a fuzz transmission. keep trying until we are successful, whenever a failure # occurs, restart the target. From 1b39ce6cd40600d93ca2baf0983b7eb5b688cda6 Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 21 Aug 2015 15:38:25 -0700 Subject: [PATCH 08/15] Fixes: fuzz_data_logger is now optional (previous tests were broken!). Removed leftover debugging change. --- sulley/sessions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sulley/sessions.py b/sulley/sessions.py index 60c80c1..4514ba7 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -207,11 +207,11 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg @type crash_threshold: int @kwarg crash_threshold (Optional, def=3) Maximum number of crashes allowed before a node is exhaust @type restart_sleep_time: int - @kwarg restart_sleep_time: Optional, def=300) Time in seconds to sleep when target can't be restarted + @kwarg restart_sleep_time: (Optional, def=300) Time in seconds to sleep when target can't be restarted @type web_port: int @kwarg web_port: (Optional, def=26000) Port for monitoring fuzzing campaign via a web browser @type fuzz_data_logger: ifuzz_logger.IFuzzLogger - @kwarg fuzz_data_logger: For saving data sent to and from the target. + @kwarg fuzz_data_logger: (Optional, def=None) For saving data sent to and from the target. """ super(Session, self).__init__() @@ -483,7 +483,8 @@ def error_handler(error, msg, error_target, error_sock=None): # if we don't need to skip the current test case. if self.total_mutant_index > self.skip: self.logger.info("fuzzing %d of %d" % (self.fuzz_node.mutant_index, num_mutations)) - self._fuzz_data_logger.open_test_case(self.total_mutant_index) + if self._fuzz_data_logger is not None: + self._fuzz_data_logger.open_test_case(self.total_mutant_index) # attempt to complete a fuzz transmission. keep trying until we are successful, whenever a failure # occurs, restart the target. @@ -530,7 +531,7 @@ def error_handler(error, msg, error_target, error_sock=None): self.transmit(target, self.fuzz_node, edge) except Exception, e: error_handler(e, "failed transmitting fuzz node", target, target) - raise + continue # if we reach this point the send was successful for break out of the while(1). break From a9a05608253080c2c0f9ac16ae1150bf14486894 Mon Sep 17 00:00:00 2001 From: josh Date: Thu, 27 Aug 2015 10:18:41 -0700 Subject: [PATCH 09/15] Added a message_terminator parameter to SerialTarget. --- sulley/serial_connection.py | 15 +++++++++++++-- sulley/serial_target.py | 14 +++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/sulley/serial_connection.py b/sulley/serial_connection.py index d9521e6..a9d23b2 100644 --- a/sulley/serial_connection.py +++ b/sulley/serial_connection.py @@ -1,15 +1,17 @@ import itarget_connection import serial +import re class SerialConnection(itarget_connection.ITargetConnection): """ ITargetConnection implementation using serial ports. - Messages are time-delimited, based on a parameter given to the constructor. + Messages are time-delimited, based on a parameter given to the constructor, and may also be delimited by a + specific message terminator regex. """ - def __init__(self, port, baudrate, message_separator_time=0.300): + def __init__(self, port, baudrate, message_separator_time=0.300, message_terminator=None): """ @type port: int | str @param port: Serial port name or number. @@ -17,12 +19,18 @@ def __init__(self, port, baudrate, message_separator_time=0.300): @param baudrate: Baud rate for port. @type message_separator_time: float @param message_separator_time: The amount of time to wait before considering a reply from the target complete. + This is different than a timeout, as the message is considered complete, not + timed out. + @type message_terminator: str + @param message_terminator: (Optional, def=None) Regex string to search for. When found, indicates a + completed message. """ self._device = None self.port = port self.baudrate = baudrate self.logger = None self.message_separator_time = message_separator_time + self.message_terminator = message_terminator def close(self): """ @@ -58,6 +66,9 @@ def recv(self, max_bytes): # Serial ports can be slow and render only a few bytes at a time. # Therefore, we keep reading until we get nothing, in hopes of getting a full packet. while fragment: + # Quit if we find the message terminator + if self.message_terminator is not None and re.search(self.message_terminator, data) is not None: + break fragment = self._device.read(size=1024) data += fragment diff --git a/sulley/serial_target.py b/sulley/serial_target.py index e8d6d5c..39cdde3 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -4,13 +4,14 @@ class SerialTarget(sessions.Target): """ - Target class that uses a SerailConnection. Serial messages are assumed to be time-separated. + Target class that uses a SerailConnection. Serial messages are assumed to be time-separated, terminated by a + separator string/regex, or both. Encapsulates connection logic for the target, as well as pedrpc connection logic. Contains a logger which is configured by Session.add_target(). """ - def __init__(self, port=0, baudrate=9600, message_separator_time=0.300): + def __init__(self, port=0, baudrate=9600, message_separator_time=0.300, message_terminator=None): """ @type port: int | str @param port: Serial port name or number. @@ -18,13 +19,20 @@ def __init__(self, port=0, baudrate=9600, message_separator_time=0.300): @param baudrate: Baud rate for port. @type message_separator_time: float @param message_separator_time: The amount of time to wait before considering a reply from the target complete. + This is different than a timeout, as the message is considered complete, not + timed out. + @type message_terminator: str + @param message_terminator: (Optional, def=None) Regex string to search for. When found, indicates a + completed message. """ super(SerialTarget, self).__init__(host="", port=1) self._target_connection = serial_connection.SerialConnection( port=port, baudrate=baudrate, - message_separator_time=message_separator_time) + message_separator_time=message_separator_time, + message_terminator=message_terminator + ) # set these manually once target is instantiated. self.netmon = None From a395c801dc903347d05159db0593cce67b61c36e Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 28 Aug 2015 14:12:41 -0700 Subject: [PATCH 10/15] In progress: abstracted out two serial connection classes and started unit tests. --- sulley/serial_connection.py | 96 --------- sulley/serial_connection_generic.py | 116 +++++++++++ sulley/serial_connection_low_level.py | 76 +++++++ sulley/serial_target.py | 4 +- unit_tests/test_serial_connection_generic.py | 196 +++++++++++++++++++ 5 files changed, 390 insertions(+), 98 deletions(-) delete mode 100644 sulley/serial_connection.py create mode 100644 sulley/serial_connection_generic.py create mode 100644 sulley/serial_connection_low_level.py create mode 100644 unit_tests/test_serial_connection_generic.py diff --git a/sulley/serial_connection.py b/sulley/serial_connection.py deleted file mode 100644 index a9d23b2..0000000 --- a/sulley/serial_connection.py +++ /dev/null @@ -1,96 +0,0 @@ -import itarget_connection -import serial -import re - - -class SerialConnection(itarget_connection.ITargetConnection): - """ - ITargetConnection implementation using serial ports. - - Messages are time-delimited, based on a parameter given to the constructor, and may also be delimited by a - specific message terminator regex. - """ - - def __init__(self, port, baudrate, message_separator_time=0.300, message_terminator=None): - """ - @type port: int | str - @param port: Serial port name or number. - @type baudrate: int - @param baudrate: Baud rate for port. - @type message_separator_time: float - @param message_separator_time: The amount of time to wait before considering a reply from the target complete. - This is different than a timeout, as the message is considered complete, not - timed out. - @type message_terminator: str - @param message_terminator: (Optional, def=None) Regex string to search for. When found, indicates a - completed message. - """ - self._device = None - self.port = port - self.baudrate = baudrate - self.logger = None - self.message_separator_time = message_separator_time - self.message_terminator = message_terminator - - def close(self): - """ - Close connection to the target. - - :return: None - """ - self._device.close() - - def open(self): - """ - Opens connection to the target. Make sure to call close! - - :return: None - """ - self._device = serial.Serial(port=self.port, baudrate=self.baudrate) - - def recv(self, max_bytes): - """ - Receive up to max_bytes data from the target. - - :param max_bytes: Maximum number of bytes to receive. - :type max_bytes: int - - :return: Received data. - """ - - self._device.timeout = self.message_separator_time - - fragment = self._device.read(size=1024) - data = fragment - - # Serial ports can be slow and render only a few bytes at a time. - # Therefore, we keep reading until we get nothing, in hopes of getting a full packet. - while fragment: - # Quit if we find the message terminator - if self.message_terminator is not None and re.search(self.message_terminator, data) is not None: - break - fragment = self._device.read(size=1024) - data += fragment - - return data - - def send(self, data): - """ - Send data to the target. Only valid after calling open! - - :param data: Data to send. - - :return: None - """ - self._device.write(data) - - def set_logger(self, logger): - """ - Set this object's (and it's aggregated classes') logger. - - :param logger: Logger to use. - :type logger: logging.Logger - - :return: None - """ - self.logger = logger diff --git a/sulley/serial_connection_generic.py b/sulley/serial_connection_generic.py new file mode 100644 index 0000000..ae80aaa --- /dev/null +++ b/sulley/serial_connection_generic.py @@ -0,0 +1,116 @@ +import itarget_connection + + +class SerialConnectionGeneric(itarget_connection.ITargetConnection): + """ + ITargetConnection implementation using serial ports. Designed to utilize SerialConnectionLowLevel. + + Since serial ports provide no default functionality for separating messages/packets, this class provides + several means: + - timeout: Return received bytes after timeout seconds. + - msg_separator_time: + Return received bytes after the wire is silent for a given time. + This is useful, e.g., for terminal protocols without a machine-readable delimiter. + A response may take a long time to send its information, and you know the message is done + when data stops coming. + - content_check: + A user-defined function takes the data received so far and checks for a packet. + The function should return 0 if the packet isn't finished yet, or n if a valid message of n + bytes has been received. Remaining bytes are stored for next call to recv(). + + Example: + def content_check_newline(data): + if data.find('\n') >= 0: + return data.find('\n') + else: + return 0 + If none of these methods are used, your connection may hang forever. + """ + + def __init__(self, connection, timeout=None, message_separator_time=None, content_checker=None): + """ + @type connection: itarget_connection.ITargetConnection + @param connection: Low level connection, e.g., SerialConnectionLowLevel. + @type timeout: float + @param timeout: For recv(). After timeout seconds from receive start, + recv() will return all received data, if any. + @type message_separator_time: float + @param message_separator_time: After message_separator_time seconds _without receiving any more data_, + recv() will return. + @type content_checker: function(str) -> int + @param content_checker: (Optional, def=None) User-defined function. + recv() will pass received bytes to this method. + If the method returns n > 0, recv() will return n bytes. + If it returns 0, recv() will keep on reading. + """ + self._connection = connection + self._logger = None + self.timeout = timeout + self.message_separator_time = message_separator_time + self.content_checker = content_checker + + def close(self): + """ + Close connection to the target. + + :return: None + """ + self._connection.close() + + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + self._connection.open() + + def recv(self, max_bytes): + """ + Receive up to max_bytes data from the target. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + + self._connection.timeout = min(.001, self.message_separator_time, self.timeout) + + fragment = self._connection.recv(max_bytes=max_bytes) + data = fragment + # + # # Serial ports can be slow and render only a few bytes at a time. + # # Therefore, we keep reading until we get nothing, in hopes of getting a full packet. + # while fragment: + # # Quit if we find the message terminator + # if self.message_terminator is not None and re.search(self.message_terminator, data) is not None: + # break + # fragment = self._device.read(size=1024) + # data += fragment + + return data + + def send(self, data): + """ + Send data to the target. Only valid after calling open! + + :param data: Data to send. + + :return: None + """ + bytes_sent = 0 + while bytes_sent < len(data): + bytes_sent += self._connection.send(data[bytes_sent:]) + return bytes_sent + + def set_logger(self, logger): + """ + Set this object's (and it's aggregated classes') logger. + + :param logger: Logger to use. + :type logger: logging.Logger + + :return: None + """ + self._logger = logger diff --git a/sulley/serial_connection_low_level.py b/sulley/serial_connection_low_level.py new file mode 100644 index 0000000..938ae31 --- /dev/null +++ b/sulley/serial_connection_low_level.py @@ -0,0 +1,76 @@ +import itarget_connection +import serial + + +class SerialConnection(itarget_connection.ITargetConnection): + """ + A basic wrapper for a serial object. This class provides no + Separated from SerialConnectionGeneric to allow for effective unit testing. + Implements ITargetConnection. + """ + + def __init__(self, port, baudrate, timeout=None): + """ + @type port: int | str + @param port: Serial port name or number. + @type baudrate: int + @param baudrate: Baud rate for port. + @type timeout: float + @param timeout: Serial port timeout. See pySerial docs. May be updated after creation. + """ + self._device = None + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self._logger = None + + def close(self): + """ + Close connection to the target. + + :return: None + """ + self._device.close() + + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + self._device = serial.Serial(port=self.port, baudrate=self.baudrate) + + def recv(self, max_bytes): + """ + Receive up to max_bytes data from the target. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + self._device.timeout = self.timeout + return self._device.read(size=max_bytes) + + def send(self, data): + """ + Send data to the target. Only valid after calling open! + + :param data: Data to send. + + :return: None + """ + self._device.write(data) + + def set_logger(self, logger): + """ + Set this object's (and it's aggregated classes') logger. + + Note: Logger is unused, but necessary to fulfill ITargetConnection. + + :param logger: Logger to use. + :type logger: logging.Logger + + :return: None + """ + self._logger = logger diff --git a/sulley/serial_target.py b/sulley/serial_target.py index 39cdde3..ca8654e 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -1,5 +1,5 @@ import sessions -import serial_connection +import serial_connection_low_level class SerialTarget(sessions.Target): @@ -27,7 +27,7 @@ def __init__(self, port=0, baudrate=9600, message_separator_time=0.300, message_ """ super(SerialTarget, self).__init__(host="", port=1) - self._target_connection = serial_connection.SerialConnection( + self._target_connection = serial_connection_low_level.SerialConnection( port=port, baudrate=baudrate, message_separator_time=message_separator_time, diff --git a/unit_tests/test_serial_connection_generic.py b/unit_tests/test_serial_connection_generic.py new file mode 100644 index 0000000..87a5998 --- /dev/null +++ b/unit_tests/test_serial_connection_generic.py @@ -0,0 +1,196 @@ +import unittest +from sulley import itarget_connection +from sulley.serial_connection_generic import SerialConnectionGeneric +import random + + +class MockSerialConnection(itarget_connection.ITargetConnection): + def __init__(self): + self.close_called = False + self.open_called = False + self.send_data_list = [] + self.send_return_queue = [] + + def close(self): + """ + Close connection. + + :return: None + """ + self.close_called = True + + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + self.open_called = True + + def recv(self, max_bytes): + """ + Receive up to max_bytes data. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. bytes('') if no data is received. + """ + raise NotImplementedError + + def send(self, data): + """ + Send data to the target. + + :param data: Data to send. + + :return: None + """ + self.send_data_list.append(data) + if len(self.send_return_queue) > 0: + return self.send_return_queue.pop(0) + else: + return len(data) + + +class TestSerialConnection(unittest.TestCase): + def setUp(self): + self.mock = MockSerialConnection() + + def test_open(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.open(). + Then: MockSerialConnection.open() is called. + """ + uut = SerialConnectionGeneric(connection=self.mock) + uut.open() + self.assertTrue(self.mock.open_called) + + def test_close(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.close(). + Then: MockSerialConnection.close() is called. + """ + uut = SerialConnectionGeneric(connection=self.mock) + uut.close() + self.assertTrue(self.mock.close_called) + + def test_send_basic(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) + and: MockSerialConnection.send() returns len(data). + Then: Verify MockSerialConnection.send() was called only once. + and: Verify MockSerialConnection.send() received the expected data. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = "ABCDEFG" + uut.send(data=data) + # Then + self.assertEqual(len(self.mock.send_data_list), 1) + self.assertEqual(self.mock.send_data_list[0], "ABCDEFG") + + def test_send_multiple(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 9 bytes. + and: MockSerialConnection.send() returns: 0, 0, 1, 2, 3, 2, 1. + Then: Verify MockSerialConnection.send() was called exactly 7 times. + and: Verify MockSerialConnection.send() received the expected data each time. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = "123456789" + self.mock.send_return_queue = [0, 0, 1, 2, 3, 2, 1] + uut.send(data=data) + # Then + self.assertEqual(len(self.mock.send_data_list), 7) + self.assertEqual(self.mock.send_data_list, ["123456789", + "123456789", + "123456789", + "23456789", + "456789", + "789", + "9"]) + + def test_send_off_by_one(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 9 bytes. + and: MockSerialConnection.send() returns: 8, 1. + Then: Verify MockSerialConnection.send() was called exactly 2 times. + and: Verify MockSerialConnection.send() received the expected data each time. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = "123456789" + self.mock.send_return_queue = [8, 1] + uut.send(data=data) + # Then + self.assertEqual(len(self.mock.send_data_list), 2) + self.assertEqual(self.mock.send_data_list, ["123456789", + "9"]) + + def test_send_one_byte(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 1 byte. + and: MockSerialConnection.send() returns: 0, 1. + Then: Verify MockSerialConnection.send() was called exactly 2 times. + and: Verify MockSerialConnection.send() received the expected data each time. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = "1" + self.mock.send_return_queue = [0, 1] + uut.send(data=data) + # Then + self.assertEqual(len(self.mock.send_data_list), 2) + self.assertEqual(self.mock.send_data_list, ["1", + "1"]) + + def test_send_many(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + + When: Calling SerialConnectionGeneric.send(data) with 9 bytes. + and: MockSerialConnection.send() returns: 0, 500 times, followed by len(data). + + Then: Verify MockSerialConnection.send() was called exactly 501 times. + and: Verify MockSerialConnection.send() received the expected data each time. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = "123456789" + self.mock.send_return_queue = [0]*500 + [len(data)] + uut.send(data=data) + # Then + self.assertEqual(len(self.mock.send_data_list), 501) + self.assertEqual(self.mock.send_data_list, ["123456789"]*501) + + def test_send_zero_bytes(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 0 bytes. + and: MockSerialConnection.send() set to return len(data). + Then: Verify MockSerialConnection.send() was called either 0 or 1 times. + and: Verify MockSerialConnection.send() received 0 bytes, if anything. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = "" + self.mock.send_return_queue = [0, 1] + uut.send(data=data) + # Then + self.assertLessEqual(len(self.mock.send_data_list), 1) + if len(self.mock.send_data_list) == 0: + self.assertEqual(self.mock.send_data_list, []) + else: + self.assertEqual(self.mock.send_data_list, [""]) + + +if __name__ == '__main__': + unittest.main() From 89830b566ae747698bf649d216da70daab185fad Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 31 Aug 2015 15:52:25 -0700 Subject: [PATCH 11/15] SerialConnectionGeneric.send and .recv UTs passing! --- sulley/serial_connection_generic.py | 43 ++- sulley/serial_target.py | 15 +- unit_tests/test_serial_connection_generic.py | 269 ++++++++++++++++++- 3 files changed, 294 insertions(+), 33 deletions(-) diff --git a/sulley/serial_connection_generic.py b/sulley/serial_connection_generic.py index ae80aaa..b9d4955 100644 --- a/sulley/serial_connection_generic.py +++ b/sulley/serial_connection_generic.py @@ -1,9 +1,10 @@ +import time import itarget_connection class SerialConnectionGeneric(itarget_connection.ITargetConnection): """ - ITargetConnection implementation using serial ports. Designed to utilize SerialConnectionLowLevel. + ITargetConnection implementation for generic serial ports. Designed to utilize SerialConnectionLowLevel. Since serial ports provide no default functionality for separating messages/packets, this class provides several means: @@ -49,6 +50,8 @@ def __init__(self, connection, timeout=None, message_separator_time=None, conten self.message_separator_time = message_separator_time self.content_checker = content_checker + self._leftover_bytes = b'' + def close(self): """ Close connection to the target. @@ -77,17 +80,33 @@ def recv(self, max_bytes): self._connection.timeout = min(.001, self.message_separator_time, self.timeout) - fragment = self._connection.recv(max_bytes=max_bytes) - data = fragment - # - # # Serial ports can be slow and render only a few bytes at a time. - # # Therefore, we keep reading until we get nothing, in hopes of getting a full packet. - # while fragment: - # # Quit if we find the message terminator - # if self.message_terminator is not None and re.search(self.message_terminator, data) is not None: - # break - # fragment = self._device.read(size=1024) - # data += fragment + start_time = last_byte_time = time.time() + + data = self._leftover_bytes + self._leftover_bytes = b'' + + while len(data) < max_bytes: + # Update timer for message_separator_time + if len(data) > 0: + last_byte_time = time.time() + + # Try recv again + fragment = self._connection.recv(max_bytes=max_bytes-len(data)) + data += fragment + + # User-supplied content_checker function + if self.content_checker is not None: + num_valid_bytes = self.content_checker(data) + if num_valid_bytes > 0: + self._leftover_bytes = data[num_valid_bytes:] + return data[0:num_valid_bytes] + + # Check timeout and message_separator_time + cur_time = time.time() + if self.timeout is not None and cur_time - start_time >= self.timeout: + return data + if self.message_separator_time is not None and cur_time - last_byte_time >= self.message_separator_time: + return data return data diff --git a/sulley/serial_target.py b/sulley/serial_target.py index ca8654e..7036b73 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -1,4 +1,5 @@ import sessions +import serial_connection_generic import serial_connection_low_level @@ -11,7 +12,7 @@ class SerialTarget(sessions.Target): Contains a logger which is configured by Session.add_target(). """ - def __init__(self, port=0, baudrate=9600, message_separator_time=0.300, message_terminator=None): + def __init__(self, port=0, baudrate=9600, timeout=5, message_separator_time=0.300, content_checker=None): """ @type port: int | str @param port: Serial port name or number. @@ -21,17 +22,17 @@ def __init__(self, port=0, baudrate=9600, message_separator_time=0.300, message_ @param message_separator_time: The amount of time to wait before considering a reply from the target complete. This is different than a timeout, as the message is considered complete, not timed out. - @type message_terminator: str - @param message_terminator: (Optional, def=None) Regex string to search for. When found, indicates a + @type content_checker: str + @param content_checker: (Optional, def=None) Regex string to search for. When found, indicates a completed message. """ super(SerialTarget, self).__init__(host="", port=1) - self._target_connection = serial_connection_low_level.SerialConnection( - port=port, - baudrate=baudrate, + self._target_connection = serial_connection_generic.SerialConnectionGeneric( + connection=serial_connection_low_level.SerialConnection(port=port, baudrate=baudrate), + timeout=timeout, message_separator_time=message_separator_time, - message_terminator=message_terminator + content_checker=message_separator_time ) # set these manually once target is instantiated. diff --git a/unit_tests/test_serial_connection_generic.py b/unit_tests/test_serial_connection_generic.py index 87a5998..d1e1633 100644 --- a/unit_tests/test_serial_connection_generic.py +++ b/unit_tests/test_serial_connection_generic.py @@ -1,7 +1,7 @@ import unittest from sulley import itarget_connection from sulley.serial_connection_generic import SerialConnectionGeneric -import random +import time class MockSerialConnection(itarget_connection.ITargetConnection): @@ -10,6 +10,10 @@ def __init__(self): self.open_called = False self.send_data_list = [] self.send_return_queue = [] + self.recv_max_bytes_lengths = [] + self.recv_return_queue = [] + self.recv_return_nothing_by_default = False + self.recv_wait_times = [] def close(self): """ @@ -36,7 +40,20 @@ def recv(self, max_bytes): :return: Received data. bytes('') if no data is received. """ - raise NotImplementedError + # Wait if needed + if len(self.recv_wait_times) > 0: + time.sleep(self.recv_wait_times.pop(0)) + + # Save argument + self.recv_max_bytes_lengths.append(max_bytes) + + # Return data + if len(self.recv_return_queue) > 0: + return self.recv_return_queue.pop(0) + elif self.recv_return_nothing_by_default: + return b'' + else: + return [0]*max_bytes def send(self, data): """ @@ -77,13 +94,18 @@ def test_close(self): uut.close() self.assertTrue(self.mock.close_called) + ########################################################################### + # Send tests + ########################################################################### def test_send_basic(self): """ Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) - and: MockSerialConnection.send() returns len(data). + and: MockSerialConnection.send() returns len(data). + Then: Verify MockSerialConnection.send() was called only once. - and: Verify MockSerialConnection.send() received the expected data. + and: Verify MockSerialConnection.send() received the expected data. """ uut = SerialConnectionGeneric(connection=self.mock) # When @@ -96,10 +118,12 @@ def test_send_basic(self): def test_send_multiple(self): """ Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 0, 0, 1, 2, 3, 2, 1. + and: MockSerialConnection.send() returns: 0, 0, 1, 2, 3, 2, 1. + Then: Verify MockSerialConnection.send() was called exactly 7 times. - and: Verify MockSerialConnection.send() received the expected data each time. + and: Verify MockSerialConnection.send() received the expected data each time. """ uut = SerialConnectionGeneric(connection=self.mock) # When @@ -119,10 +143,12 @@ def test_send_multiple(self): def test_send_off_by_one(self): """ Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 8, 1. + and: MockSerialConnection.send() returns: 8, 1. + Then: Verify MockSerialConnection.send() was called exactly 2 times. - and: Verify MockSerialConnection.send() received the expected data each time. + and: Verify MockSerialConnection.send() received the expected data each time. """ uut = SerialConnectionGeneric(connection=self.mock) # When @@ -137,10 +163,12 @@ def test_send_off_by_one(self): def test_send_one_byte(self): """ Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 1 byte. - and: MockSerialConnection.send() returns: 0, 1. + and: MockSerialConnection.send() returns: 0, 1. + Then: Verify MockSerialConnection.send() was called exactly 2 times. - and: Verify MockSerialConnection.send() received the expected data each time. + and: Verify MockSerialConnection.send() received the expected data each time. """ uut = SerialConnectionGeneric(connection=self.mock) # When @@ -157,10 +185,10 @@ def test_send_many(self): Given: A SerialConnectionGeneric using MockSerialConnection. When: Calling SerialConnectionGeneric.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 0, 500 times, followed by len(data). + and: MockSerialConnection.send() returns: 0, 500 times, followed by len(data). Then: Verify MockSerialConnection.send() was called exactly 501 times. - and: Verify MockSerialConnection.send() received the expected data each time. + and: Verify MockSerialConnection.send() received the expected data each time. """ uut = SerialConnectionGeneric(connection=self.mock) # When @@ -174,10 +202,12 @@ def test_send_many(self): def test_send_zero_bytes(self): """ Given: A SerialConnectionGeneric using MockSerialConnection. + When: Calling SerialConnectionGeneric.send(data) with 0 bytes. - and: MockSerialConnection.send() set to return len(data). + and: MockSerialConnection.send() set to return len(data). + Then: Verify MockSerialConnection.send() was called either 0 or 1 times. - and: Verify MockSerialConnection.send() received 0 bytes, if anything. + and: Verify MockSerialConnection.send() received 0 bytes, if anything. """ uut = SerialConnectionGeneric(connection=self.mock) # When @@ -191,6 +221,217 @@ def test_send_zero_bytes(self): else: self.assertEqual(self.mock.send_data_list, [""]) + ########################################################################### + # Receive tests + ########################################################################### + def test_recv_simple(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection, + with no timeout/message_separator_time/content_checker. + + When: User calls SerialConnectionGeneric.recv. + and: MockSerialConnection.recv set to return data of length max_bytes. + + Then: SerialConnectionGeneric calls MockSerialConnection.recv exactly once. + and: SerialConnectionGeneric.recv returns exactly what MockSerialConnection.recv returned. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + self.mock.recv_return_queue = ["0123456"] + data = uut.recv(max_bytes=7) + # Then + self.assertEqual(self.mock.recv_max_bytes_lengths, [7]) + self.assertEqual(data, "0123456") + + def test_recv_max_bytes_only(self): + """ + Given: A SerialConnectionGeneric using MockSerialConnection, + with no timeout/message_separator_time/content_checker. + + When: User calls SerialConnectionGeneric.recv(10). + and: MockSerialConnection.recv set to return 0, 0, 0, 1, 2, 3, 4 bytes. + + Then: SerialConnectionGeneric calls MockSerialConnection.recv exactly 7 times, + with max_bytes decreasing as appropriate. + and: SerialConnectionGeneric.recv returns the concatenation of MockSerialConnection.recv() return values. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + self.mock.recv_return_queue = ["", "", "", "1", "22", "123", "1234"] + data = uut.recv(max_bytes=10) + # Then + self.assertEqual(self.mock.recv_max_bytes_lengths, [10, 10, 10, 10, 9, 7, 4]) + self.assertEqual(data, b"1221231234") + + def test_recv_timeout(self): + """ + Verify that recv() returns partial messages after the timeout expires. + + Given: A SerialConnectionGeneric using MockSerialConnection, + with timeout set to a smallish value. + + When: User calls SerialConnectionGeneric.recv(n) several times with different values of n. + and: MockSerialConnection.recv set to return a single message, then repeatedly return nothing. + + Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv at least once. + and: SerialConnectionGeneric.recv returns the MockSerialConnection.recv() return value after the timeout. + + Note: Timeout functionality is tested, but not the precise timing. + """ + uut = SerialConnectionGeneric(connection=self.mock, timeout=.001) # 1ms + + # n == 1 + self.mock.recv_return_nothing_by_default = True + self.mock.recv_return_queue = [b''] + data = uut.recv(max_bytes=1) + self.assertGreaterEqual(len(self.mock.recv_max_bytes_lengths), 1) + self.assertEqual(data, b'') + + # n == 2 + self.mock.recv_return_nothing_by_default = True + self.mock.recv_return_queue = [b'1'] + data = uut.recv(max_bytes=2) + self.assertGreaterEqual(len(self.mock.recv_max_bytes_lengths), 1) + self.assertEqual(data, b'1') + + # n == 3, len(data) == 1 + self.mock.recv_return_nothing_by_default = True + self.mock.recv_return_queue = [b'1'] + data = uut.recv(max_bytes=5) + self.assertGreaterEqual(len(self.mock.recv_max_bytes_lengths), 1) + self.assertEqual(data, b'1') + + # n == 3, len(data) == 2 + self.mock.recv_return_nothing_by_default = True + self.mock.recv_return_queue = [b'12'] + data = uut.recv(max_bytes=3) + self.assertGreaterEqual(len(self.mock.recv_max_bytes_lengths), 1) + self.assertEqual(data, b'12') + + # # n == 2**16, len(data) == 2**16 - 1 + # self.mock.recv_return_nothing_by_default = True + # self.mock.recv_return_queue = [b'\0'] * (2**16 - 1) + # data = uut.recv(max_bytes=2**16) + # self.assertGreaterEqual(len(self.mock.recv_max_bytes_lengths), 1) + # self.assertEqual(data, [b'\0'] * (2**16 - 1)) + + def test_recv_message_separator_time(self): + """ + Verify that message_separator_time works correctly. + Receive a message over time t > message_separator_time, where each part of the message is delayed by + t' < message_separator_time. + + Given: A SerialConnectionGeneric using MockSerialConnection, + and: timeout set to 60ms. + and: message_separator_time set 20ms + + When: User calls SerialConnectionGeneric.recv(60). + and: MockSerialConnection.recv set to return increasing bytes. + and: MockSerialConnection.recv set to delay 1ms on each call. + + Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv more than 20 times. + and: SerialConnectionGeneric.recv returns data with more than 20 bytes. + """ + # Given + uut = SerialConnectionGeneric(connection=self.mock, timeout=.060, message_separator_time=.020) + + # When + self.mock.recv_return_queue = [b"1"] * 60 + self.mock.recv_wait_times = [.001] * 60 + data = uut.recv(max_bytes=60) + + # Then + self.assertGreater(len(self.mock.recv_max_bytes_lengths), 20) + self.assertGreater(len(data), 20) + + def test_recv_message_separator_time_2(self): + """ + Verify that message_separator_time works correctly. + Receive a message that times out with message_separator_time, but which would not time out with only a timeout. + + Given: A SerialConnectionGeneric using MockSerialConnection, + and: timeout set to 60ms. + and: message_separator_time set 20ms + + When: User calls SerialConnectionGeneric.recv(60). + and: MockSerialConnection.recv set to return 1 byte, then 1 byte, then 58 bytes. + and: MockSerialConnection.recv set to delay 1ms, then 40ms, then 1ms. + + Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv twice. + and: SerialConnectionGeneric.recv returns only the first two bytes. + """ + # Given + uut = SerialConnectionGeneric(connection=self.mock, timeout=.060, message_separator_time=.020) + + # When + self.mock.recv_return_queue = [b"1", b"2", b"3"*58] + self.mock.recv_wait_times = [.001, .040, .001] + data = uut.recv(max_bytes=60) + + # Then + self.assertEqual(len(self.mock.recv_max_bytes_lengths), 2) + self.assertEqual(data, b"12") + + def test_recv_message_content_checker(self): + """ + Verify that content_checker is used correctly. + The content_checker indicates how much of a message is valid, if any. + Verify behavior when the content_checker consumes a part of the buffer, the full buffer, and then part of it + again. + + Given: A SerialConnectionGeneric using MockSerialConnection, + and: timeout set to 100ms. + and: message_separator_time set 20ms + and: content_checker set to a function that returns 0, 3, 0, 5, 0, 3 + + When: User calls SerialConnectionGeneric.recv(100) 3 times. + and: MockSerialConnection.recv set to return 2 bytes repeatedly. + + Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv 6 times. + and: SerialConnectionGeneric.recv returns only the first 3 bytes, then the next 5 bytes, then the next 3. + """ + # Given + def test_checker(*args, **kwargs): + """ + :param args: Ignored. Makes method callable with arguments. + :param kwargs: Ignored. Makes method callable with arguments. + + :return: 0, 3, 0, 5, 0, 3, 0, 0... + """ + if not hasattr(test_checker, "counter"): + test_checker.counter = 0 + + test_checker.counter += 1 + + if test_checker.counter == 2: + return 3 + elif test_checker.counter == 4: + return 5 + elif test_checker.counter == 6: + return 3 + else: + return 0 + + uut = SerialConnectionGeneric(connection=self.mock, + timeout=.100, + message_separator_time=.020, + content_checker=test_checker) + + # When + self.mock.recv_return_queue = [b"12", b"34", b"56", b"78", b"9A", b"BC"] + + data = uut.recv(max_bytes=100) + self.assertEqual(len(self.mock.recv_max_bytes_lengths), 2) + self.assertEqual(data, b"123") + + data = uut.recv(max_bytes=100) + self.assertEqual(len(self.mock.recv_max_bytes_lengths), 4) + self.assertEqual(data, b"45678") + + data = uut.recv(max_bytes=100) + self.assertEqual(len(self.mock.recv_max_bytes_lengths), 6) + self.assertEqual(data, b"9AB") + if __name__ == '__main__': unittest.main() From 122602735ba0740c8cce60c87ef9ba55d4f1b034 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 1 Sep 2015 09:05:01 -0700 Subject: [PATCH 12/15] Debugging and docstring improvements in SerialConnection files. --- sulley/serial_connection_generic.py | 4 +- sulley/serial_connection_low_level.py | 2 +- sulley/serial_target.py | 2 +- unit_tests/test_serial_connection_generic.py | 107 +++++++++++++------ 4 files changed, 77 insertions(+), 38 deletions(-) diff --git a/sulley/serial_connection_generic.py b/sulley/serial_connection_generic.py index b9d4955..4f4d494 100644 --- a/sulley/serial_connection_generic.py +++ b/sulley/serial_connection_generic.py @@ -120,7 +120,9 @@ def send(self, data): """ bytes_sent = 0 while bytes_sent < len(data): - bytes_sent += self._connection.send(data[bytes_sent:]) + bytes_sent_this_round = self._connection.send(data[bytes_sent:]) + if bytes_sent_this_round is not None: + bytes_sent += bytes_sent_this_round return bytes_sent def set_logger(self, logger): diff --git a/sulley/serial_connection_low_level.py b/sulley/serial_connection_low_level.py index 938ae31..3798d3b 100644 --- a/sulley/serial_connection_low_level.py +++ b/sulley/serial_connection_low_level.py @@ -60,7 +60,7 @@ def send(self, data): :return: None """ - self._device.write(data) + return self._device.write(data) def set_logger(self, logger): """ diff --git a/sulley/serial_target.py b/sulley/serial_target.py index 7036b73..f5db335 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -32,7 +32,7 @@ def __init__(self, port=0, baudrate=9600, timeout=5, message_separator_time=0.30 connection=serial_connection_low_level.SerialConnection(port=port, baudrate=baudrate), timeout=timeout, message_separator_time=message_separator_time, - content_checker=message_separator_time + content_checker=content_checker ) # set these manually once target is instantiated. diff --git a/unit_tests/test_serial_connection_generic.py b/unit_tests/test_serial_connection_generic.py index d1e1633..5929faf 100644 --- a/unit_tests/test_serial_connection_generic.py +++ b/unit_tests/test_serial_connection_generic.py @@ -109,39 +109,66 @@ def test_send_basic(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - data = "ABCDEFG" + data = b'ABCDEFG' uut.send(data=data) # Then self.assertEqual(len(self.mock.send_data_list), 1) - self.assertEqual(self.mock.send_data_list[0], "ABCDEFG") + self.assertEqual(self.mock.send_data_list[0], b'ABCDEFG') + + def test_send_return_none(self): + """ + Verify that MockSerialConnection.send() is called again when it returns None. + + Given: A SerialConnectionGeneric using MockSerialConnection. + + When: Calling SerialConnectionGeneric.send(data) with 10 bytes. + and: MockSerialConnection.send() returns: None, 10. + + Then: Verify MockSerialConnection.send() was called exactly 2 times. + and: Verify MockSerialConnection.send() received the expected data each time. + """ + uut = SerialConnectionGeneric(connection=self.mock) + # When + data = b'123456789A' + self.mock.send_return_queue = [None, 10] + uut.send(data=data) + # Then + self.assertEqual(len(self.mock.send_data_list), 2) + self.assertEqual(self.mock.send_data_list, [b'123456789A', + b'123456789A']) def test_send_multiple(self): """ + Verify that MockSerialConnection.send() is called repeatedly until it sends all the data. + Given: A SerialConnectionGeneric using MockSerialConnection. When: Calling SerialConnectionGeneric.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 0, 0, 1, 2, 3, 2, 1. + and: MockSerialConnection.send() returns: 0, None, 0, 1, 2, 3, 2, 1. Then: Verify MockSerialConnection.send() was called exactly 7 times. and: Verify MockSerialConnection.send() received the expected data each time. """ uut = SerialConnectionGeneric(connection=self.mock) # When - data = "123456789" - self.mock.send_return_queue = [0, 0, 1, 2, 3, 2, 1] + data = b'123456789' + self.mock.send_return_queue = [0, None, 0, 1, 2, 3, 2, 1] uut.send(data=data) # Then - self.assertEqual(len(self.mock.send_data_list), 7) - self.assertEqual(self.mock.send_data_list, ["123456789", - "123456789", - "123456789", - "23456789", - "456789", - "789", - "9"]) + self.assertEqual(len(self.mock.send_data_list), 8) + self.assertEqual(self.mock.send_data_list, [b'123456789', + b'123456789', + b'123456789', + b'123456789', + b'23456789', + b'456789', + b'789', + b'9']) def test_send_off_by_one(self): """ + Verify that MockSerialConnection.send() is called again when it sends all but 1 byte. + Given: A SerialConnectionGeneric using MockSerialConnection. When: Calling SerialConnectionGeneric.send(data) with 9 bytes. @@ -152,16 +179,18 @@ def test_send_off_by_one(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - data = "123456789" + data = b'123456789' self.mock.send_return_queue = [8, 1] uut.send(data=data) # Then self.assertEqual(len(self.mock.send_data_list), 2) - self.assertEqual(self.mock.send_data_list, ["123456789", - "9"]) + self.assertEqual(self.mock.send_data_list, [b'123456789', + b'9']) def test_send_one_byte(self): """ + Verify that MockSerialConnection.send() is called again when it returns 0 after being given 1 byte. + Given: A SerialConnectionGeneric using MockSerialConnection. When: Calling SerialConnectionGeneric.send(data) with 1 byte. @@ -172,16 +201,18 @@ def test_send_one_byte(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - data = "1" + data = b'1' self.mock.send_return_queue = [0, 1] uut.send(data=data) # Then self.assertEqual(len(self.mock.send_data_list), 2) - self.assertEqual(self.mock.send_data_list, ["1", - "1"]) + self.assertEqual(self.mock.send_data_list, [b'1', + b'1']) def test_send_many(self): """ + Verify that send works properly when MockSerialConnection.send() sends 1 byte at a time. + Given: A SerialConnectionGeneric using MockSerialConnection. When: Calling SerialConnectionGeneric.send(data) with 9 bytes. @@ -192,15 +223,17 @@ def test_send_many(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - data = "123456789" + data = b'123456789' self.mock.send_return_queue = [0]*500 + [len(data)] uut.send(data=data) # Then self.assertEqual(len(self.mock.send_data_list), 501) - self.assertEqual(self.mock.send_data_list, ["123456789"]*501) + self.assertEqual(self.mock.send_data_list, [b'123456789']*501) def test_send_zero_bytes(self): """ + Verify that send() doesn't fail when given 0 bytes. + Given: A SerialConnectionGeneric using MockSerialConnection. When: Calling SerialConnectionGeneric.send(data) with 0 bytes. @@ -211,7 +244,7 @@ def test_send_zero_bytes(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - data = "" + data = b'' self.mock.send_return_queue = [0, 1] uut.send(data=data) # Then @@ -219,13 +252,15 @@ def test_send_zero_bytes(self): if len(self.mock.send_data_list) == 0: self.assertEqual(self.mock.send_data_list, []) else: - self.assertEqual(self.mock.send_data_list, [""]) + self.assertEqual(self.mock.send_data_list, [b'']) ########################################################################### # Receive tests ########################################################################### def test_recv_simple(self): """ + Verify that recv() works in the normal case. + Given: A SerialConnectionGeneric using MockSerialConnection, with no timeout/message_separator_time/content_checker. @@ -237,14 +272,16 @@ def test_recv_simple(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - self.mock.recv_return_queue = ["0123456"] + self.mock.recv_return_queue = [b'0123456'] data = uut.recv(max_bytes=7) # Then self.assertEqual(self.mock.recv_max_bytes_lengths, [7]) - self.assertEqual(data, "0123456") + self.assertEqual(data, b'0123456') def test_recv_max_bytes_only(self): """ + Verify that recv() calls MockSerialConnection.recv() repeatedly until it gets max_bytes of data. + Given: A SerialConnectionGeneric using MockSerialConnection, with no timeout/message_separator_time/content_checker. @@ -257,11 +294,11 @@ def test_recv_max_bytes_only(self): """ uut = SerialConnectionGeneric(connection=self.mock) # When - self.mock.recv_return_queue = ["", "", "", "1", "22", "123", "1234"] + self.mock.recv_return_queue = [b'', b'', b'', b'1', b'22', b'123', b'1234'] data = uut.recv(max_bytes=10) # Then self.assertEqual(self.mock.recv_max_bytes_lengths, [10, 10, 10, 10, 9, 7, 4]) - self.assertEqual(data, b"1221231234") + self.assertEqual(data, b'1221231234') def test_recv_timeout(self): """ @@ -318,7 +355,7 @@ def test_recv_timeout(self): def test_recv_message_separator_time(self): """ Verify that message_separator_time works correctly. - Receive a message over time t > message_separator_time, where each part of the message is delayed by + Receive a message over time t, where t > message_separator_time, and each part of the message is delayed by t' < message_separator_time. Given: A SerialConnectionGeneric using MockSerialConnection, @@ -336,7 +373,7 @@ def test_recv_message_separator_time(self): uut = SerialConnectionGeneric(connection=self.mock, timeout=.060, message_separator_time=.020) # When - self.mock.recv_return_queue = [b"1"] * 60 + self.mock.recv_return_queue = [b'1'] * 60 self.mock.recv_wait_times = [.001] * 60 data = uut.recv(max_bytes=60) @@ -364,13 +401,13 @@ def test_recv_message_separator_time_2(self): uut = SerialConnectionGeneric(connection=self.mock, timeout=.060, message_separator_time=.020) # When - self.mock.recv_return_queue = [b"1", b"2", b"3"*58] + self.mock.recv_return_queue = [b'1', b'2', b'3'*58] self.mock.recv_wait_times = [.001, .040, .001] data = uut.recv(max_bytes=60) # Then self.assertEqual(len(self.mock.recv_max_bytes_lengths), 2) - self.assertEqual(data, b"12") + self.assertEqual(data, b'12') def test_recv_message_content_checker(self): """ @@ -418,19 +455,19 @@ def test_checker(*args, **kwargs): content_checker=test_checker) # When - self.mock.recv_return_queue = [b"12", b"34", b"56", b"78", b"9A", b"BC"] + self.mock.recv_return_queue = [b'12', b'34', b'56', b'78', b'9A', b'BC'] data = uut.recv(max_bytes=100) self.assertEqual(len(self.mock.recv_max_bytes_lengths), 2) - self.assertEqual(data, b"123") + self.assertEqual(data, b'123') data = uut.recv(max_bytes=100) self.assertEqual(len(self.mock.recv_max_bytes_lengths), 4) - self.assertEqual(data, b"45678") + self.assertEqual(data, b'45678') data = uut.recv(max_bytes=100) self.assertEqual(len(self.mock.recv_max_bytes_lengths), 6) - self.assertEqual(data, b"9AB") + self.assertEqual(data, b'9AB') if __name__ == '__main__': From 916891e6d086535bb4d45162c790799994510994 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 1 Sep 2015 09:32:40 -0700 Subject: [PATCH 13/15] Rename refactors for Serial modules. --- ...ection_generic.py => serial_connection.py} | 2 +- sulley/serial_connection_low_level.py | 6 +- sulley/serial_target.py | 6 +- sulley/sessions.py | 2 +- unit_tests/test_serial_connection_generic.py | 118 +++++++++--------- 5 files changed, 68 insertions(+), 66 deletions(-) rename sulley/{serial_connection_generic.py => serial_connection.py} (98%) diff --git a/sulley/serial_connection_generic.py b/sulley/serial_connection.py similarity index 98% rename from sulley/serial_connection_generic.py rename to sulley/serial_connection.py index 4f4d494..6acac50 100644 --- a/sulley/serial_connection_generic.py +++ b/sulley/serial_connection.py @@ -2,7 +2,7 @@ import itarget_connection -class SerialConnectionGeneric(itarget_connection.ITargetConnection): +class SerialConnection(itarget_connection.ITargetConnection): """ ITargetConnection implementation for generic serial ports. Designed to utilize SerialConnectionLowLevel. diff --git a/sulley/serial_connection_low_level.py b/sulley/serial_connection_low_level.py index 3798d3b..7a0ef00 100644 --- a/sulley/serial_connection_low_level.py +++ b/sulley/serial_connection_low_level.py @@ -2,10 +2,10 @@ import serial -class SerialConnection(itarget_connection.ITargetConnection): +class SerialConnectionLowLevel(itarget_connection.ITargetConnection): """ - A basic wrapper for a serial object. This class provides no - Separated from SerialConnectionGeneric to allow for effective unit testing. + A basic wrapper for a serial object. + Separated from SerialConnection to allow for effective unit testing. Implements ITargetConnection. """ diff --git a/sulley/serial_target.py b/sulley/serial_target.py index f5db335..e32d68c 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -1,5 +1,5 @@ import sessions -import serial_connection_generic +import serial_connection import serial_connection_low_level @@ -28,8 +28,8 @@ def __init__(self, port=0, baudrate=9600, timeout=5, message_separator_time=0.30 """ super(SerialTarget, self).__init__(host="", port=1) - self._target_connection = serial_connection_generic.SerialConnectionGeneric( - connection=serial_connection_low_level.SerialConnection(port=port, baudrate=baudrate), + self._target_connection = serial_connection.SerialConnection( + connection=serial_connection_low_level.SerialConnectionLowLevel(port=port, baudrate=baudrate), timeout=timeout, message_separator_time=message_separator_time, content_checker=content_checker diff --git a/sulley/sessions.py b/sulley/sessions.py index 4514ba7..2bfa82c 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -524,7 +524,7 @@ def error_handler(error, msg, error_target, error_sock=None): self.transmit(target, node, e) except Exception, e: error_handler(e, "failed transmitting a node up the path", target, target) - continue + raise # now send the current node we are fuzzing. try: diff --git a/unit_tests/test_serial_connection_generic.py b/unit_tests/test_serial_connection_generic.py index 5929faf..3453d94 100644 --- a/unit_tests/test_serial_connection_generic.py +++ b/unit_tests/test_serial_connection_generic.py @@ -1,6 +1,6 @@ import unittest from sulley import itarget_connection -from sulley.serial_connection_generic import SerialConnectionGeneric +from sulley.serial_connection import SerialConnection import time @@ -76,21 +76,21 @@ def setUp(self): def test_open(self): """ - Given: A SerialConnectionGeneric using MockSerialConnection. - When: Calling SerialConnectionGeneric.open(). + Given: A SerialConnection using MockSerialConnection. + When: Calling SerialConnection.open(). Then: MockSerialConnection.open() is called. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) uut.open() self.assertTrue(self.mock.open_called) def test_close(self): """ - Given: A SerialConnectionGeneric using MockSerialConnection. - When: Calling SerialConnectionGeneric.close(). + Given: A SerialConnection using MockSerialConnection. + When: Calling SerialConnection.close(). Then: MockSerialConnection.close() is called. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) uut.close() self.assertTrue(self.mock.close_called) @@ -99,15 +99,15 @@ def test_close(self): ########################################################################### def test_send_basic(self): """ - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) + When: Calling SerialConnection.send(data) and: MockSerialConnection.send() returns len(data). Then: Verify MockSerialConnection.send() was called only once. and: Verify MockSerialConnection.send() received the expected data. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'ABCDEFG' uut.send(data=data) @@ -119,15 +119,15 @@ def test_send_return_none(self): """ Verify that MockSerialConnection.send() is called again when it returns None. - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) with 10 bytes. + When: Calling SerialConnection.send(data) with 10 bytes. and: MockSerialConnection.send() returns: None, 10. Then: Verify MockSerialConnection.send() was called exactly 2 times. and: Verify MockSerialConnection.send() received the expected data each time. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'123456789A' self.mock.send_return_queue = [None, 10] @@ -141,15 +141,15 @@ def test_send_multiple(self): """ Verify that MockSerialConnection.send() is called repeatedly until it sends all the data. - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) with 9 bytes. + When: Calling SerialConnection.send(data) with 9 bytes. and: MockSerialConnection.send() returns: 0, None, 0, 1, 2, 3, 2, 1. Then: Verify MockSerialConnection.send() was called exactly 7 times. and: Verify MockSerialConnection.send() received the expected data each time. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'123456789' self.mock.send_return_queue = [0, None, 0, 1, 2, 3, 2, 1] @@ -169,15 +169,15 @@ def test_send_off_by_one(self): """ Verify that MockSerialConnection.send() is called again when it sends all but 1 byte. - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) with 9 bytes. + When: Calling SerialConnection.send(data) with 9 bytes. and: MockSerialConnection.send() returns: 8, 1. Then: Verify MockSerialConnection.send() was called exactly 2 times. and: Verify MockSerialConnection.send() received the expected data each time. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'123456789' self.mock.send_return_queue = [8, 1] @@ -191,15 +191,15 @@ def test_send_one_byte(self): """ Verify that MockSerialConnection.send() is called again when it returns 0 after being given 1 byte. - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) with 1 byte. + When: Calling SerialConnection.send(data) with 1 byte. and: MockSerialConnection.send() returns: 0, 1. Then: Verify MockSerialConnection.send() was called exactly 2 times. and: Verify MockSerialConnection.send() received the expected data each time. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'1' self.mock.send_return_queue = [0, 1] @@ -213,15 +213,15 @@ def test_send_many(self): """ Verify that send works properly when MockSerialConnection.send() sends 1 byte at a time. - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) with 9 bytes. + When: Calling SerialConnection.send(data) with 9 bytes. and: MockSerialConnection.send() returns: 0, 500 times, followed by len(data). Then: Verify MockSerialConnection.send() was called exactly 501 times. and: Verify MockSerialConnection.send() received the expected data each time. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'123456789' self.mock.send_return_queue = [0]*500 + [len(data)] @@ -234,15 +234,15 @@ def test_send_zero_bytes(self): """ Verify that send() doesn't fail when given 0 bytes. - Given: A SerialConnectionGeneric using MockSerialConnection. + Given: A SerialConnection using MockSerialConnection. - When: Calling SerialConnectionGeneric.send(data) with 0 bytes. + When: Calling SerialConnection.send(data) with 0 bytes. and: MockSerialConnection.send() set to return len(data). Then: Verify MockSerialConnection.send() was called either 0 or 1 times. and: Verify MockSerialConnection.send() received 0 bytes, if anything. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When data = b'' self.mock.send_return_queue = [0, 1] @@ -261,16 +261,16 @@ def test_recv_simple(self): """ Verify that recv() works in the normal case. - Given: A SerialConnectionGeneric using MockSerialConnection, + Given: A SerialConnection using MockSerialConnection, with no timeout/message_separator_time/content_checker. - When: User calls SerialConnectionGeneric.recv. + When: User calls SerialConnection.recv. and: MockSerialConnection.recv set to return data of length max_bytes. - Then: SerialConnectionGeneric calls MockSerialConnection.recv exactly once. - and: SerialConnectionGeneric.recv returns exactly what MockSerialConnection.recv returned. + Then: SerialConnection calls MockSerialConnection.recv exactly once. + and: SerialConnection.recv returns exactly what MockSerialConnection.recv returned. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When self.mock.recv_return_queue = [b'0123456'] data = uut.recv(max_bytes=7) @@ -282,17 +282,17 @@ def test_recv_max_bytes_only(self): """ Verify that recv() calls MockSerialConnection.recv() repeatedly until it gets max_bytes of data. - Given: A SerialConnectionGeneric using MockSerialConnection, + Given: A SerialConnection using MockSerialConnection, with no timeout/message_separator_time/content_checker. - When: User calls SerialConnectionGeneric.recv(10). + When: User calls SerialConnection.recv(10). and: MockSerialConnection.recv set to return 0, 0, 0, 1, 2, 3, 4 bytes. - Then: SerialConnectionGeneric calls MockSerialConnection.recv exactly 7 times, + Then: SerialConnection calls MockSerialConnection.recv exactly 7 times, with max_bytes decreasing as appropriate. - and: SerialConnectionGeneric.recv returns the concatenation of MockSerialConnection.recv() return values. + and: SerialConnection.recv returns the concatenation of MockSerialConnection.recv() return values. """ - uut = SerialConnectionGeneric(connection=self.mock) + uut = SerialConnection(connection=self.mock) # When self.mock.recv_return_queue = [b'', b'', b'', b'1', b'22', b'123', b'1234'] data = uut.recv(max_bytes=10) @@ -304,18 +304,18 @@ def test_recv_timeout(self): """ Verify that recv() returns partial messages after the timeout expires. - Given: A SerialConnectionGeneric using MockSerialConnection, + Given: A SerialConnection using MockSerialConnection, with timeout set to a smallish value. - When: User calls SerialConnectionGeneric.recv(n) several times with different values of n. + When: User calls SerialConnection.recv(n) several times with different values of n. and: MockSerialConnection.recv set to return a single message, then repeatedly return nothing. - Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv at least once. - and: SerialConnectionGeneric.recv returns the MockSerialConnection.recv() return value after the timeout. + Then: SerialConnection.recv calls MockSerialConnection.recv at least once. + and: SerialConnection.recv returns the MockSerialConnection.recv() return value after the timeout. Note: Timeout functionality is tested, but not the precise timing. """ - uut = SerialConnectionGeneric(connection=self.mock, timeout=.001) # 1ms + uut = SerialConnection(connection=self.mock, timeout=.001) # 1ms # n == 1 self.mock.recv_return_nothing_by_default = True @@ -358,19 +358,19 @@ def test_recv_message_separator_time(self): Receive a message over time t, where t > message_separator_time, and each part of the message is delayed by t' < message_separator_time. - Given: A SerialConnectionGeneric using MockSerialConnection, + Given: A SerialConnection using MockSerialConnection, and: timeout set to 60ms. and: message_separator_time set 20ms - When: User calls SerialConnectionGeneric.recv(60). + When: User calls SerialConnection.recv(60). and: MockSerialConnection.recv set to return increasing bytes. and: MockSerialConnection.recv set to delay 1ms on each call. - Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv more than 20 times. - and: SerialConnectionGeneric.recv returns data with more than 20 bytes. + Then: SerialConnection.recv calls MockSerialConnection.recv more than 20 times. + and: SerialConnection.recv returns data with more than 20 bytes. """ # Given - uut = SerialConnectionGeneric(connection=self.mock, timeout=.060, message_separator_time=.020) + uut = SerialConnection(connection=self.mock, timeout=.060, message_separator_time=.020) # When self.mock.recv_return_queue = [b'1'] * 60 @@ -386,19 +386,19 @@ def test_recv_message_separator_time_2(self): Verify that message_separator_time works correctly. Receive a message that times out with message_separator_time, but which would not time out with only a timeout. - Given: A SerialConnectionGeneric using MockSerialConnection, + Given: A SerialConnection using MockSerialConnection, and: timeout set to 60ms. and: message_separator_time set 20ms - When: User calls SerialConnectionGeneric.recv(60). + When: User calls SerialConnection.recv(60). and: MockSerialConnection.recv set to return 1 byte, then 1 byte, then 58 bytes. and: MockSerialConnection.recv set to delay 1ms, then 40ms, then 1ms. - Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv twice. - and: SerialConnectionGeneric.recv returns only the first two bytes. + Then: SerialConnection.recv calls MockSerialConnection.recv twice. + and: SerialConnection.recv returns only the first two bytes. """ # Given - uut = SerialConnectionGeneric(connection=self.mock, timeout=.060, message_separator_time=.020) + uut = SerialConnection(connection=self.mock, timeout=.060, message_separator_time=.020) # When self.mock.recv_return_queue = [b'1', b'2', b'3'*58] @@ -416,18 +416,20 @@ def test_recv_message_content_checker(self): Verify behavior when the content_checker consumes a part of the buffer, the full buffer, and then part of it again. - Given: A SerialConnectionGeneric using MockSerialConnection, + Given: A SerialConnection using MockSerialConnection, and: timeout set to 100ms. and: message_separator_time set 20ms and: content_checker set to a function that returns 0, 3, 0, 5, 0, 3 - When: User calls SerialConnectionGeneric.recv(100) 3 times. + When: User calls SerialConnection.recv(100) 3 times. and: MockSerialConnection.recv set to return 2 bytes repeatedly. - Then: SerialConnectionGeneric.recv calls MockSerialConnection.recv 6 times. - and: SerialConnectionGeneric.recv returns only the first 3 bytes, then the next 5 bytes, then the next 3. + Then: SerialConnection.recv calls MockSerialConnection.recv 6 times. + and: SerialConnection.recv returns only the first 3 bytes, then the next 5 bytes, then the next 3. """ # Given + # PyUnusedLocal suppression: args/kwargs make the method callable by SerialConnection, but are not used. + # noinspection PyUnusedLocal def test_checker(*args, **kwargs): """ :param args: Ignored. Makes method callable with arguments. @@ -449,7 +451,7 @@ def test_checker(*args, **kwargs): else: return 0 - uut = SerialConnectionGeneric(connection=self.mock, + uut = SerialConnection(connection=self.mock, timeout=.100, message_separator_time=.020, content_checker=test_checker) From f6f9246d12d9ca57e2087ffbd3071fba34ff3900 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 1 Sep 2015 10:04:45 -0700 Subject: [PATCH 14/15] Added ISerialLike interface for SerialConnectionLowLevel. Docstring fixes. --- sulley/iserial_like.py | 52 +++++++ sulley/serial_connection.py | 11 +- sulley/serial_connection_low_level.py | 22 +-- sulley/serial_target.py | 29 ++-- unit_tests/test_serial_connection_generic.py | 154 ++++++++++--------- 5 files changed, 166 insertions(+), 102 deletions(-) create mode 100644 sulley/iserial_like.py diff --git a/sulley/iserial_like.py b/sulley/iserial_like.py new file mode 100644 index 0000000..cf236fa --- /dev/null +++ b/sulley/iserial_like.py @@ -0,0 +1,52 @@ +import abc + + +class ISerialLike(object): + """ + A serial-like interface, based on the pySerial module, + the notable difference being that open() must always be called after the object is first created. + + Facilitates dependency injection in modules that use pySerial. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def close(self): + """ + Close connection to the target. + + :return: None + """ + raise NotImplementedError + + @abc.abstractmethod + def open(self): + """ + Opens connection to the target. Make sure to call close! + + :return: None + """ + raise NotImplementedError + + @abc.abstractmethod + def recv(self, max_bytes): + """ + Receive up to max_bytes data from the target. + + :param max_bytes: Maximum number of bytes to receive. + :type max_bytes: int + + :return: Received data. + """ + raise NotImplementedError + + @abc.abstractmethod + def send(self, data): + """ + Send data to the target. Only valid after calling open! + + :param data: Data to send. + + :return: Number of bytes actually sent. + """ + raise NotImplementedError diff --git a/sulley/serial_connection.py b/sulley/serial_connection.py index 6acac50..908acf0 100644 --- a/sulley/serial_connection.py +++ b/sulley/serial_connection.py @@ -1,10 +1,12 @@ import time import itarget_connection +import iserial_like class SerialConnection(itarget_connection.ITargetConnection): """ - ITargetConnection implementation for generic serial ports. Designed to utilize SerialConnectionLowLevel. + ITargetConnection implementation for generic serial ports. + Designed to utilize SerialConnectionLowLevel (see __init__). Since serial ports provide no default functionality for separating messages/packets, this class provides several means: @@ -30,17 +32,18 @@ def content_check_newline(data): def __init__(self, connection, timeout=None, message_separator_time=None, content_checker=None): """ - @type connection: itarget_connection.ITargetConnection + @type connection: iserial_like.ISerialLike @param connection: Low level connection, e.g., SerialConnectionLowLevel. @type timeout: float @param timeout: For recv(). After timeout seconds from receive start, recv() will return all received data, if any. @type message_separator_time: float - @param message_separator_time: After message_separator_time seconds _without receiving any more data_, + @param message_separator_time: (Optional, def=None) + After message_separator_time seconds _without receiving any more data_, recv() will return. @type content_checker: function(str) -> int @param content_checker: (Optional, def=None) User-defined function. - recv() will pass received bytes to this method. + recv() will pass all bytes received so far to this method. If the method returns n > 0, recv() will return n bytes. If it returns 0, recv() will keep on reading. """ diff --git a/sulley/serial_connection_low_level.py b/sulley/serial_connection_low_level.py index 7a0ef00..5fa1256 100644 --- a/sulley/serial_connection_low_level.py +++ b/sulley/serial_connection_low_level.py @@ -1,12 +1,12 @@ -import itarget_connection +import iserial_like import serial -class SerialConnectionLowLevel(itarget_connection.ITargetConnection): +class SerialConnectionLowLevel(iserial_like.ISerialLike): """ A basic wrapper for a serial object. Separated from SerialConnection to allow for effective unit testing. - Implements ITargetConnection. + Implements serial_like.ISerialLike. """ def __init__(self, port, baudrate, timeout=None): @@ -22,7 +22,6 @@ def __init__(self, port, baudrate, timeout=None): self.port = port self.baudrate = baudrate self.timeout = timeout - self._logger = None def close(self): """ @@ -58,19 +57,6 @@ def send(self, data): :param data: Data to send. - :return: None + :return: Number of bytes actually sent. """ return self._device.write(data) - - def set_logger(self, logger): - """ - Set this object's (and it's aggregated classes') logger. - - Note: Logger is unused, but necessary to fulfill ITargetConnection. - - :param logger: Logger to use. - :type logger: logging.Logger - - :return: None - """ - self._logger = logger diff --git a/sulley/serial_target.py b/sulley/serial_target.py index e32d68c..d73c773 100644 --- a/sulley/serial_target.py +++ b/sulley/serial_target.py @@ -5,26 +5,37 @@ class SerialTarget(sessions.Target): """ - Target class that uses a SerailConnection. Serial messages are assumed to be time-separated, terminated by a - separator string/regex, or both. - Encapsulates connection logic for the target, as well as pedrpc connection logic. + Target class that uses a serial_connection.SerialConnection. Serial messages are assumed to be time-separated, + terminated by a separator string/regex, or both. + Encapsulates connection logic for the target. Inherits pedrpc connection logic from sessions.Target. Contains a logger which is configured by Session.add_target(). """ def __init__(self, port=0, baudrate=9600, timeout=5, message_separator_time=0.300, content_checker=None): """ + See serial_connection.SerialConnection for details on timeout, message_separator_time, and content_checker. + @type port: int | str @param port: Serial port name or number. + @type baudrate: int @param baudrate: Baud rate for port. + + @type timeout: float + @param timeout: For recv(). After timeout seconds from receive start, + recv() will return all received data, if any. + @type message_separator_time: float - @param message_separator_time: The amount of time to wait before considering a reply from the target complete. - This is different than a timeout, as the message is considered complete, not - timed out. - @type content_checker: str - @param content_checker: (Optional, def=None) Regex string to search for. When found, indicates a - completed message. + @param message_separator_time: (Optional, def=None) + After message_separator_time seconds _without receiving any more data_, + recv() will return. + + @type content_checker: function(str) -> int + @param content_checker: (Optional, def=None) User-defined function. + recv() will pass all bytes received so far to this method. + If the method returns n > 0, recv() will return n bytes. + If it returns 0, recv() will keep on reading. """ super(SerialTarget, self).__init__(host="", port=1) diff --git a/unit_tests/test_serial_connection_generic.py b/unit_tests/test_serial_connection_generic.py index 3453d94..1f183b2 100644 --- a/unit_tests/test_serial_connection_generic.py +++ b/unit_tests/test_serial_connection_generic.py @@ -1,10 +1,15 @@ import unittest -from sulley import itarget_connection +from sulley import iserial_like from sulley.serial_connection import SerialConnection import time -class MockSerialConnection(itarget_connection.ITargetConnection): +class MockSerial(iserial_like.ISerialLike): + """ + Mock ISerialLike class. + Methods include code for unit testing. See each method for details. + """ + def __init__(self): self.close_called = False self.open_called = False @@ -35,6 +40,13 @@ def recv(self, max_bytes): """ Receive up to max_bytes data. + Mock method: + - Waits some amount of time according to self.recv_wait_times + - Appends max_bytes to self.recv_max_bytes_lengths + - Returns based on self.recv_return_queue + * If empty, returns b'' if self.recv_return_nothing_by_default is True, or + b'0'*max_bytes otherwise. + :param max_bytes: Maximum number of bytes to receive. :type max_bytes: int @@ -53,7 +65,7 @@ def recv(self, max_bytes): elif self.recv_return_nothing_by_default: return b'' else: - return [0]*max_bytes + return b'0' * max_bytes def send(self, data): """ @@ -72,13 +84,13 @@ def send(self, data): class TestSerialConnection(unittest.TestCase): def setUp(self): - self.mock = MockSerialConnection() + self.mock = MockSerial() def test_open(self): """ - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.open(). - Then: MockSerialConnection.open() is called. + Then: MockSerial.open() is called. """ uut = SerialConnection(connection=self.mock) uut.open() @@ -86,9 +98,9 @@ def test_open(self): def test_close(self): """ - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.close(). - Then: MockSerialConnection.close() is called. + Then: MockSerial.close() is called. """ uut = SerialConnection(connection=self.mock) uut.close() @@ -99,13 +111,13 @@ def test_close(self): ########################################################################### def test_send_basic(self): """ - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) - and: MockSerialConnection.send() returns len(data). + and: MockSerial.send() returns len(data). - Then: Verify MockSerialConnection.send() was called only once. - and: Verify MockSerialConnection.send() received the expected data. + Then: Verify MockSerial.send() was called only once. + and: Verify MockSerial.send() received the expected data. """ uut = SerialConnection(connection=self.mock) # When @@ -117,15 +129,15 @@ def test_send_basic(self): def test_send_return_none(self): """ - Verify that MockSerialConnection.send() is called again when it returns None. + Verify that MockSerial.send() is called again when it returns None. - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) with 10 bytes. - and: MockSerialConnection.send() returns: None, 10. + and: MockSerial.send() returns: None, 10. - Then: Verify MockSerialConnection.send() was called exactly 2 times. - and: Verify MockSerialConnection.send() received the expected data each time. + Then: Verify MockSerial.send() was called exactly 2 times. + and: Verify MockSerial.send() received the expected data each time. """ uut = SerialConnection(connection=self.mock) # When @@ -139,15 +151,15 @@ def test_send_return_none(self): def test_send_multiple(self): """ - Verify that MockSerialConnection.send() is called repeatedly until it sends all the data. + Verify that MockSerial.send() is called repeatedly until it sends all the data. - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 0, None, 0, 1, 2, 3, 2, 1. + and: MockSerial.send() returns: 0, None, 0, 1, 2, 3, 2, 1. - Then: Verify MockSerialConnection.send() was called exactly 7 times. - and: Verify MockSerialConnection.send() received the expected data each time. + Then: Verify MockSerial.send() was called exactly 7 times. + and: Verify MockSerial.send() received the expected data each time. """ uut = SerialConnection(connection=self.mock) # When @@ -167,15 +179,15 @@ def test_send_multiple(self): def test_send_off_by_one(self): """ - Verify that MockSerialConnection.send() is called again when it sends all but 1 byte. + Verify that MockSerial.send() is called again when it sends all but 1 byte. - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 8, 1. + and: MockSerial.send() returns: 8, 1. - Then: Verify MockSerialConnection.send() was called exactly 2 times. - and: Verify MockSerialConnection.send() received the expected data each time. + Then: Verify MockSerial.send() was called exactly 2 times. + and: Verify MockSerial.send() received the expected data each time. """ uut = SerialConnection(connection=self.mock) # When @@ -189,15 +201,15 @@ def test_send_off_by_one(self): def test_send_one_byte(self): """ - Verify that MockSerialConnection.send() is called again when it returns 0 after being given 1 byte. + Verify that MockSerial.send() is called again when it returns 0 after being given 1 byte. - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) with 1 byte. - and: MockSerialConnection.send() returns: 0, 1. + and: MockSerial.send() returns: 0, 1. - Then: Verify MockSerialConnection.send() was called exactly 2 times. - and: Verify MockSerialConnection.send() received the expected data each time. + Then: Verify MockSerial.send() was called exactly 2 times. + and: Verify MockSerial.send() received the expected data each time. """ uut = SerialConnection(connection=self.mock) # When @@ -211,36 +223,36 @@ def test_send_one_byte(self): def test_send_many(self): """ - Verify that send works properly when MockSerialConnection.send() sends 1 byte at a time. + Verify that send works properly when MockSerial.send() sends 1 byte at a time. - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) with 9 bytes. - and: MockSerialConnection.send() returns: 0, 500 times, followed by len(data). + and: MockSerial.send() returns: 0, 500 times, followed by len(data). - Then: Verify MockSerialConnection.send() was called exactly 501 times. - and: Verify MockSerialConnection.send() received the expected data each time. + Then: Verify MockSerial.send() was called exactly 501 times. + and: Verify MockSerial.send() received the expected data each time. """ uut = SerialConnection(connection=self.mock) # When data = b'123456789' - self.mock.send_return_queue = [0]*500 + [len(data)] + self.mock.send_return_queue = [0] * 500 + [len(data)] uut.send(data=data) # Then self.assertEqual(len(self.mock.send_data_list), 501) - self.assertEqual(self.mock.send_data_list, [b'123456789']*501) + self.assertEqual(self.mock.send_data_list, [b'123456789'] * 501) def test_send_zero_bytes(self): """ Verify that send() doesn't fail when given 0 bytes. - Given: A SerialConnection using MockSerialConnection. + Given: A SerialConnection using MockSerial. When: Calling SerialConnection.send(data) with 0 bytes. - and: MockSerialConnection.send() set to return len(data). + and: MockSerial.send() set to return len(data). - Then: Verify MockSerialConnection.send() was called either 0 or 1 times. - and: Verify MockSerialConnection.send() received 0 bytes, if anything. + Then: Verify MockSerial.send() was called either 0 or 1 times. + and: Verify MockSerial.send() received 0 bytes, if anything. """ uut = SerialConnection(connection=self.mock) # When @@ -261,14 +273,14 @@ def test_recv_simple(self): """ Verify that recv() works in the normal case. - Given: A SerialConnection using MockSerialConnection, + Given: A SerialConnection using MockSerial, with no timeout/message_separator_time/content_checker. When: User calls SerialConnection.recv. - and: MockSerialConnection.recv set to return data of length max_bytes. + and: MockSerial.recv set to return data of length max_bytes. - Then: SerialConnection calls MockSerialConnection.recv exactly once. - and: SerialConnection.recv returns exactly what MockSerialConnection.recv returned. + Then: SerialConnection calls MockSerial.recv exactly once. + and: SerialConnection.recv returns exactly what MockSerial.recv returned. """ uut = SerialConnection(connection=self.mock) # When @@ -280,17 +292,17 @@ def test_recv_simple(self): def test_recv_max_bytes_only(self): """ - Verify that recv() calls MockSerialConnection.recv() repeatedly until it gets max_bytes of data. + Verify that recv() calls MockSerial.recv() repeatedly until it gets max_bytes of data. - Given: A SerialConnection using MockSerialConnection, + Given: A SerialConnection using MockSerial, with no timeout/message_separator_time/content_checker. When: User calls SerialConnection.recv(10). - and: MockSerialConnection.recv set to return 0, 0, 0, 1, 2, 3, 4 bytes. + and: MockSerial.recv set to return 0, 0, 0, 1, 2, 3, 4 bytes. - Then: SerialConnection calls MockSerialConnection.recv exactly 7 times, + Then: SerialConnection calls MockSerial.recv exactly 7 times, with max_bytes decreasing as appropriate. - and: SerialConnection.recv returns the concatenation of MockSerialConnection.recv() return values. + and: SerialConnection.recv returns the concatenation of MockSerial.recv() return values. """ uut = SerialConnection(connection=self.mock) # When @@ -304,14 +316,14 @@ def test_recv_timeout(self): """ Verify that recv() returns partial messages after the timeout expires. - Given: A SerialConnection using MockSerialConnection, + Given: A SerialConnection using MockSerial, with timeout set to a smallish value. When: User calls SerialConnection.recv(n) several times with different values of n. - and: MockSerialConnection.recv set to return a single message, then repeatedly return nothing. + and: MockSerial.recv set to return a single message, then repeatedly return nothing. - Then: SerialConnection.recv calls MockSerialConnection.recv at least once. - and: SerialConnection.recv returns the MockSerialConnection.recv() return value after the timeout. + Then: SerialConnection.recv calls MockSerial.recv at least once. + and: SerialConnection.recv returns the MockSerial.recv() return value after the timeout. Note: Timeout functionality is tested, but not the precise timing. """ @@ -358,15 +370,15 @@ def test_recv_message_separator_time(self): Receive a message over time t, where t > message_separator_time, and each part of the message is delayed by t' < message_separator_time. - Given: A SerialConnection using MockSerialConnection, + Given: A SerialConnection using MockSerial, and: timeout set to 60ms. and: message_separator_time set 20ms When: User calls SerialConnection.recv(60). - and: MockSerialConnection.recv set to return increasing bytes. - and: MockSerialConnection.recv set to delay 1ms on each call. + and: MockSerial.recv set to return increasing bytes. + and: MockSerial.recv set to delay 1ms on each call. - Then: SerialConnection.recv calls MockSerialConnection.recv more than 20 times. + Then: SerialConnection.recv calls MockSerial.recv more than 20 times. and: SerialConnection.recv returns data with more than 20 bytes. """ # Given @@ -386,22 +398,22 @@ def test_recv_message_separator_time_2(self): Verify that message_separator_time works correctly. Receive a message that times out with message_separator_time, but which would not time out with only a timeout. - Given: A SerialConnection using MockSerialConnection, + Given: A SerialConnection using MockSerial, and: timeout set to 60ms. and: message_separator_time set 20ms When: User calls SerialConnection.recv(60). - and: MockSerialConnection.recv set to return 1 byte, then 1 byte, then 58 bytes. - and: MockSerialConnection.recv set to delay 1ms, then 40ms, then 1ms. + and: MockSerial.recv set to return 1 byte, then 1 byte, then 58 bytes. + and: MockSerial.recv set to delay 1ms, then 40ms, then 1ms. - Then: SerialConnection.recv calls MockSerialConnection.recv twice. + Then: SerialConnection.recv calls MockSerial.recv twice. and: SerialConnection.recv returns only the first two bytes. """ # Given uut = SerialConnection(connection=self.mock, timeout=.060, message_separator_time=.020) # When - self.mock.recv_return_queue = [b'1', b'2', b'3'*58] + self.mock.recv_return_queue = [b'1', b'2', b'3' * 58] self.mock.recv_wait_times = [.001, .040, .001] data = uut.recv(max_bytes=60) @@ -416,15 +428,15 @@ def test_recv_message_content_checker(self): Verify behavior when the content_checker consumes a part of the buffer, the full buffer, and then part of it again. - Given: A SerialConnection using MockSerialConnection, + Given: A SerialConnection using MockSerial, and: timeout set to 100ms. and: message_separator_time set 20ms and: content_checker set to a function that returns 0, 3, 0, 5, 0, 3 When: User calls SerialConnection.recv(100) 3 times. - and: MockSerialConnection.recv set to return 2 bytes repeatedly. + and: MockSerial.recv set to return 2 bytes repeatedly. - Then: SerialConnection.recv calls MockSerialConnection.recv 6 times. + Then: SerialConnection.recv calls MockSerial.recv 6 times. and: SerialConnection.recv returns only the first 3 bytes, then the next 5 bytes, then the next 3. """ # Given @@ -452,9 +464,9 @@ def test_checker(*args, **kwargs): return 0 uut = SerialConnection(connection=self.mock, - timeout=.100, - message_separator_time=.020, - content_checker=test_checker) + timeout=.100, + message_separator_time=.020, + content_checker=test_checker) # When self.mock.recv_return_queue = [b'12', b'34', b'56', b'78', b'9A', b'BC'] From 4bcef4d2473d36729f7de5d80bb69d4b37af5389 Mon Sep 17 00:00:00 2001 From: josh Date: Thu, 17 Sep 2015 17:02:00 -0700 Subject: [PATCH 15/15] Added Session.last_send to facilitate post_send methods. last_recv already exists. --- sulley/sessions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sulley/sessions.py b/sulley/sessions.py index 2bfa82c..4177442 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -269,6 +269,7 @@ def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logg self.root.name = "__ROOT_NODE__" self.root.label = self.root.name self.last_recv = None + self.last_send = None self.add_node(self.root) @@ -834,6 +835,7 @@ def transmit(self, sock, node, edge): # Try to send payload down-range try: self.targets[0].send(data) + self.last_send = data except socket.error, inst: self.logger.error("Socket error on send: %s" % inst)