From e0dc80e1a8a5bf44df6c23d0edc53693d1113aea Mon Sep 17 00:00:00 2001 From: Rossen Georgiev Date: Fri, 26 Dec 2014 23:09:29 +0000 Subject: [PATCH] test suite for IS.py + tweaks --- Makefile | 2 +- aprslib/IS.py | 42 ++++-- req.txt | 1 + tests/test_IS.py | 375 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 403 insertions(+), 17 deletions(-) create mode 100644 tests/test_IS.py diff --git a/Makefile b/Makefile index 6cf3868..0ab0993 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ init: pip install -r req.txt test: - nosetests --verbosity 2 --with-coverage + nosetests --verbosity 2 --with-coverage --cover-package=aprslib pylint: pylint -r n -f colorized aprslib || true diff --git a/aprslib/IS.py b/aprslib/IS.py index 5baeb7b..6ef0850 100644 --- a/aprslib/IS.py +++ b/aprslib/IS.py @@ -53,6 +53,7 @@ class IS(object): """ self.logger = logging.getLogger(__name__) + self._parse = parse self.set_server(host, port) self.set_login(callsign, passwd) @@ -87,9 +88,12 @@ class IS(object): """ self.server = (host, port) - def connect(self, blocking=False): + def connect(self, blocking=False, retry=30): """ Initiate connection to APRS server and attempt to login + + blocking = False - Should we block until connected and logged-in + retry = 30 - Retry interval in seconds """ if self._connected: @@ -104,7 +108,7 @@ class IS(object): if not blocking: raise - time.sleep(30) # attempt to reconnect after 30 seconds + time.sleep(retry) def close(self): """ @@ -143,7 +147,7 @@ class IS(object): if raw: callback(line) else: - callback(parse(line)) + callback(self._parse(line)) else: self.logger.debug("Server: %s", line) except (KeyboardInterrupt, SystemExit): @@ -157,7 +161,7 @@ class IS(object): self.connect(blocking=blocking) continue except GenericError: - continue + pass except StopIteration: break except: @@ -167,16 +171,21 @@ class IS(object): if not blocking: break + def _open_socket(self): + """ + Creates a socket + """ + self.sock = socket.create_connection(self.server, 15) + def _connect(self): """ - Attemps to open a connection to the server + Attemps connection to the server """ self.logger.info("Attempting connection to %s:%s", self.server[0], self.server[1]) try: - # 15 seconds connection timeout - self.sock = socket.create_connection(self.server, 15) + self._open_socket() raddr, rport = self.sock.getpeername() @@ -203,10 +212,13 @@ class IS(object): else: raise ConnectionError("invalid banner from server") - except Exception, e: + except ConnectionError: + self.close() + raise + except (socket.error, socket.timeout) as e: self.close() - if e == "timed out": + if str(e) == "timed out": raise ConnectionError("no banner from server") else: raise ConnectionError(e) @@ -232,14 +244,14 @@ class IS(object): self.sock.settimeout(5) test = self.sock.recv(len(login_str) + 100).rstrip() - self.logger.info("Server: %s", test) + self.logger.debug("Server: %s", test) (x, x, callsign, status, x) = test.split(' ', 4) if callsign == "": - raise LoginError("No callsign provided") + raise LoginError("Server responded with empty callsign???") if callsign != self.callsign: - raise LoginError("Server: %s" % test[2:]) + raise LoginError("Server: %s" % test) if status != "verified," and self.passwd != "-1": raise LoginError("Password is incorrect") @@ -261,7 +273,7 @@ class IS(object): """ try: self.sock.setblocking(0) - except socket.error, e: + except socket.error as e: raise ConnectionDrop("connection dropped") while True: @@ -275,13 +287,11 @@ class IS(object): # sock.recv returns empty if the connection drops if not short_buf: raise ConnectionDrop("connection dropped") - except socket.error, e: + except socket.error as e: if "Resource temporarily unavailable" in e: if not blocking: if len(self.buf) == 0: break - except Exception: - raise self.buf += short_buf diff --git a/req.txt b/req.txt index 0d65730..a8c4082 100644 --- a/req.txt +++ b/req.txt @@ -1,3 +1,4 @@ pylint nose coverage +mox diff --git a/tests/test_IS.py b/tests/test_IS.py new file mode 100644 index 0000000..baf8591 --- /dev/null +++ b/tests/test_IS.py @@ -0,0 +1,375 @@ +import unittest +import mox +import aprslib +import logging +import socket +import sys +import os + + +class TestCase_IS(unittest.TestCase): + def setUp(self): + self.ais = aprslib.IS("LZ1DEV-99", "testpwd", "127.0.0.1", "11111") + self.m = mox.Mox() + + def tearDown(self): + self.m.UnsetStubs() + + def test_initilization(self): + self.assertFalse(self.ais._connected) + self.assertEqual(self.ais.buf, '') + self.assertIsNone(self.ais.sock) + self.assertEqual(self.ais.callsign, "LZ1DEV-99") + self.assertEqual(self.ais.passwd, "testpwd") + self.assertEqual(self.ais.server, ("127.0.0.1", "11111")) + + def test_close(self): + self.ais._connected = True + s = mox.MockAnything() + s.close() + self.ais.sock = s + mox.Replay(s) + + self.ais.close() + + mox.Verify(s) + self.assertFalse(self.ais._connected) + self.assertEqual(self.ais.buf, '') + + def test_open_socket(self): + with self.assertRaises(socket.error): + self.ais._open_socket() + + def test_socket_readlines(self): + fdr, fdw = os.pipe() + f = os.fdopen(fdw,'w') + f.write("something") + f.close() + + self.m.ReplayAll() + self.ais.sock = mox.MockAnything() + # part 1 - conn drop before setblocking + self.ais.sock.setblocking(0).AndRaise(socket.error) + # part 2 - conn drop trying to recv + self.ais.sock.setblocking(0) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn('') + # part 3 - nothing to read + self.ais.sock.setblocking(0) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndRaise(socket.error("" + "Resource temporarily unavailable")) + # part 4 - yield 3 lines (blocking False) + self.ais.sock.setblocking(0) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("a\r\n"*3) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndRaise(socket.error("" + "Resource temporarily unavailable")) + # part 5 - yield 3 lines 2 times (blocking True) + self.ais.sock.setblocking(0) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("b\r\n"*3) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("b\r\n"*3) + self.ais.sock.fileno().AndReturn(fdr) + self.ais.sock.recv(mox.IgnoreArg()).AndRaise(StopIteration) + mox.Replay(self.ais.sock) + + # part 1 + with self.assertRaises(aprslib.exceptions.ConnectionDrop): + self.ais._socket_readlines().next() + # part 2 + with self.assertRaises(aprslib.exceptions.ConnectionDrop): + self.ais._socket_readlines().next() + # part 3 + with self.assertRaises(StopIteration): + self.ais._socket_readlines().next() + # part 4 + for line in self.ais._socket_readlines(): + self.assertEqual(line, 'a') + # part 5 + for line in self.ais._socket_readlines(blocking=True): + self.assertEqual(line, 'b') + + mox.Verify(self.ais.sock) + + def test_send_login(self): + self.ais.sock = mox.MockAnything() + self.m.StubOutWithMock(self.ais, "close") + # part 1 - raises + self.ais.sock.sendall(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("invalidreply") + self.ais.close() + # part 2 - raises (empty callsign) + self.ais.sock.sendall(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("# logresp verified, xx") + self.ais.close() + # part 3 - raises (callsign doesn't match + self.ais.sock.sendall(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("# logresp NOMATCH verified, xx") + self.ais.close() + # part 4 - raises (unverified, but pass is not -1) + self.ais.sock.sendall(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("# logresp CALL unverified, xx") + self.ais.close() + # part 5 - normal, receive only + self.ais.sock.sendall(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("# logresp CALL unverified, xx") + # part 6 - normal, correct pass + self.ais.sock.sendall(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("# logresp CALL verified, xx") + mox.Replay(self.ais.sock) + self.m.ReplayAll() + + # part 1 + self.ais.set_login("CALL", "-1") + self.assertRaises(aprslib.exceptions.LoginError, self.ais._send_login) + # part 2 + self.ais.set_login("CALL", "-1") + self.assertRaises(aprslib.exceptions.LoginError, self.ais._send_login) + # part 3 + self.ais.set_login("CALL", "-1") + self.assertRaises(aprslib.exceptions.LoginError, self.ais._send_login) + # part 4 + self.ais.set_login("CALL", "99999") + self.assertRaises(aprslib.exceptions.LoginError, self.ais._send_login) + # part 5 + self.ais.set_login("CALL", "-1") + self.ais._send_login() + # part 6 + self.ais.set_login("CALL", "99999") + self.ais._send_login() + + mox.Verify(self.ais.sock) + self.m.VerifyAll() + + def test_connect(self): + self.ais.sock = mox.MockAnything() + self.m.StubOutWithMock(self.ais, "_open_socket") + self.m.StubOutWithMock(self.ais, "close") + # part 1 - socket creation errors + self.ais._open_socket().AndRaise(socket.timeout("timed out")) + self.ais.close() + self.ais._open_socket().AndRaise(socket.error('any')) + self.ais.close() + # part 2 - invalid banner from server + self.ais._open_socket() + self.ais.sock.getpeername().AndReturn((1, 2)) + self.ais.sock.setblocking(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + if sys.platform not in ['cygwin', 'win32']: + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("junk") + self.ais.close() + # part 3 - everything going well + self.ais._open_socket() + self.ais.sock.getpeername().AndReturn((1, 2)) + self.ais.sock.setblocking(mox.IgnoreArg()) + self.ais.sock.settimeout(mox.IgnoreArg()) + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + if sys.platform not in ['cygwin', 'win32']: + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.ais.sock.setsockopt(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.ais.sock.recv(mox.IgnoreArg()).AndReturn("# server banner") + mox.Replay(self.ais.sock) + self.m.ReplayAll() + + # part 1 + self.assertRaises(aprslib.exceptions.ConnectionError, self.ais._connect) + self.assertFalse(self.ais._connected) + self.assertRaises(aprslib.exceptions.ConnectionError, self.ais._connect) + self.assertFalse(self.ais._connected) + # part 2 + self.assertRaises(aprslib.exceptions.ConnectionError, self.ais._connect) + self.assertFalse(self.ais._connected) + # part 3 + self.ais._connect() + self.assertTrue(self.ais._connected) + + mox.Verify(self.ais.sock) + self.m.VerifyAll() + + def test_filter(self): + testFilter = "x/CALLSIGN" + + self.ais._connected = True + s = mox.MockAnything() + s.sendall("#filter %s\r\n" % testFilter) + self.ais.sock = s + mox.Replay(s) + + self.ais.set_filter(testFilter) + self.assertEqual(self.ais.filter, testFilter) + + mox.Verify(s) + + def test_connect_from_notconnected(self): + self.m.StubOutWithMock(self.ais, "_connect") + self.m.StubOutWithMock(self.ais, "_send_login") + self.ais._connect() + self.ais._send_login() + self.m.ReplayAll() + + self.ais.connect() + + self.m.VerifyAll() + + def test_connect_from_connected(self): + self.m.StubOutWithMock(self.ais, "_connect") + self.m.StubOutWithMock(self.ais, "_send_login") + self.ais._connect() + self.ais._send_login() + self.m.ReplayAll() + + self.ais._connected = True + self.ais.connect() + + self.assertRaises(mox.ExpectedMethodCallsError, self.m.VerifyAll) + + def test_connect_raising_exception(self): + self.m.StubOutWithMock(self.ais, "_connect") + self.ais._connect().AndRaise(Exception("anything")) + self.m.ReplayAll() + + self.assertRaises(Exception, self.ais.connect) + + self.m.VerifyAll() + + def test_connect_raising_exceptions(self): + self.m.StubOutWithMock(self.ais, "_connect") + self.m.StubOutWithMock(self.ais, "_send_login") + self.ais._connect().AndRaise(Exception("first")) + self.ais._connect() + self.ais._send_login().AndRaise(Exception("second")) + self.ais._connect() + self.ais._send_login() + self.m.ReplayAll() + + self.ais.connect(blocking=True, retry=0) + + self.m.VerifyAll() + + +class TestCase_IS_consumer(unittest.TestCase): + def setUp(self): + self.ais = aprslib.IS("LZ1DEV-99") + self.ais._connected = True + self.m = mox.Mox() + self.m.StubOutWithMock(self.ais, "_socket_readlines") + self.m.StubOutWithMock(self.ais, "_parse") + self.m.StubOutWithMock(self.ais, "connect") + self.m.StubOutWithMock(self.ais, "close") + + def tearDown(self): + self.m.UnsetStubs() + + def test_consumer_notconnected(self): + self.ais._connected = False + + with self.assertRaises(aprslib.exceptions.ConnectionError): + self.ais.consumer(callback=lambda: None, blocking=False) + + def test_consumer_raw(self): + self.ais._socket_readlines(False).AndReturn(["line1"]) + self.m.ReplayAll() + + def testcallback(line): + self.assertEqual(line, "line1") + + self.ais.consumer(callback=testcallback, blocking=False, raw=True) + + self.m.VerifyAll() + + def test_consumer_blocking(self): + self.ais._socket_readlines(True).AndReturn(["line1"]) + self.ais._socket_readlines(True).AndReturn(["line1"] * 5) + self.ais._socket_readlines(True).AndRaise(StopIteration) + self.m.ReplayAll() + + def testcallback(line): + self.assertEqual(line, "line1") + + self.ais.consumer(callback=testcallback, blocking=True, raw=True) + + self.m.VerifyAll() + + def test_consumer_parsed(self): + self.ais._socket_readlines(False).AndReturn(["line1"]) + self.ais._parse("line1").AndReturn([]) + self.m.ReplayAll() + + def testcallback(line): + self.assertEqual(line, []) + + self.ais.consumer(callback=testcallback, blocking=False, raw=False) + + self.m.VerifyAll() + + def test_consumer_serverline(self): + self.ais._socket_readlines(False).AndReturn(["# serverline"]) + self.m.ReplayAll() + + def testcallback(line): + self.fail("callback shouldn't be called") + + self.ais.consumer(callback=testcallback, blocking=False, raw=False) + + self.m.VerifyAll() + + def test_consumer_exptions(self): + self.ais._socket_readlines(False).AndRaise(SystemExit) + self.ais._socket_readlines(False).AndRaise(KeyboardInterrupt) + self.ais._socket_readlines(False).AndRaise(Exception("random")) + self.ais._socket_readlines(False).AndRaise(StopIteration) + self.ais._socket_readlines(False).AndRaise(aprslib.exceptions.GenericError('x')) + self.m.ReplayAll() + + def testcallback(line): + pass + + for e in [SystemExit, KeyboardInterrupt, Exception]: + with self.assertRaises(e): + self.ais.consumer(callback=testcallback, blocking=False, raw=False) + + # StopIteration + self.ais.consumer(callback=testcallback, blocking=False, raw=False) + # GenericError + self.ais.consumer(callback=testcallback, blocking=False, raw=False) + + self.m.VerifyAll() + + def test_consumer_close(self): + # importal = False + self.ais._socket_readlines(False).AndRaise(aprslib.exceptions.ConnectionDrop('')) + self.ais.close() + self.ais._socket_readlines(False).AndRaise(aprslib.exceptions.ConnectionError('')) + self.ais.close() + # importal = True + self.ais._socket_readlines(False).AndRaise(aprslib.exceptions.ConnectionDrop('')) + self.ais.close() + self.ais.connect(blocking=False) + self.ais._socket_readlines(False).AndRaise(aprslib.exceptions.ConnectionError('')) + self.ais.close() + self.ais.connect(blocking=False) + self.ais._socket_readlines(False).AndRaise(StopIteration) + self.m.ReplayAll() + + with self.assertRaises(aprslib.exceptions.ConnectionDrop): + self.ais.consumer(callback=lambda: None, blocking=False, raw=False) + with self.assertRaises(aprslib.exceptions.ConnectionError): + self.ais.consumer(callback=lambda: None, blocking=False, raw=False) + + self.ais.consumer(callback=lambda: None, blocking=False, raw=False, immortal=True) + + self.m.VerifyAll()