From 258fe2358da8df6c6447a1274d4f109eb7e6aa6e Mon Sep 17 00:00:00 2001 From: Jim Paris Date: Fri, 5 Jul 2013 21:58:30 -0400 Subject: [PATCH] Filled out test coverage, fixed lots of bugs --- .coverage | Bin 71453 -> 0 bytes .coveragerc | 2 +- nilmrun/dummyfilter.py | 21 +++++++ nilmrun/processmanager.py | 59 +++++++----------- nilmrun/server.py | 23 +++++-- nilmrun/trainola.py | 0 setup.cfg | 2 +- tests/test_client.py | 125 ++++++++++++++++++++++++++++++++++---- tests/testutil/helpers.py | 4 ++ 9 files changed, 179 insertions(+), 57 deletions(-) delete mode 100644 .coverage create mode 100644 nilmrun/dummyfilter.py mode change 100755 => 100644 nilmrun/server.py mode change 100755 => 100644 nilmrun/trainola.py diff --git a/.coverage b/.coverage deleted file mode 100644 index b12e882d0da48d7e889c8f1a6a96a3a9bd910615..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71453 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 diff --git a/.coveragerc b/.coveragerc index 6de8124..a56afa8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,4 +7,4 @@ exclude_lines = pragma: no cover if 0: -omit = scripts,nilmrun/_version.py +omit = scripts,nilmrun/_version.py,nilmrun/filters/* diff --git a/nilmrun/dummyfilter.py b/nilmrun/dummyfilter.py new file mode 100644 index 0000000..40b450d --- /dev/null +++ b/nilmrun/dummyfilter.py @@ -0,0 +1,21 @@ +#!/usr/bin/python + +from nilmdb.utils.printf import * +import time +import signal +import sys + +# This is just for testing the process management. +def dummy(n): + if n < 0: # raise an exception + raise Exception("test exception") + if n == 0: # ignore SIGTERM and count to 40 + n = 40 + signal.signal(signal.SIGTERM, signal.SIG_IGN) + for x in range(n): + s = sprintf("dummy %d\n", x) + if x & 1: + sys.stdout.write(s) + else: + sys.stderr.write(s) + time.sleep(0.1) diff --git a/nilmrun/processmanager.py b/nilmrun/processmanager.py index 5d3c2af..bd12b59 100644 --- a/nilmrun/processmanager.py +++ b/nilmrun/processmanager.py @@ -9,7 +9,6 @@ import sys import os import time import uuid -import traceback class LogReceiver(object): """Spawn a thread that listens to a pipe for log messages, @@ -26,20 +25,16 @@ class LogReceiver(object): data = self.pipe.recv_bytes() self.log.write(data) except EOFError: - print "thread pipe died" self.pipe.close() - print "thread closed" - time.sleep(1) return def getvalue(self): return self.log.getvalue() def clear(self): - self.log.truncate() - self.log.seek(0) + self.log = cStringIO.StringIO() -class LogSender(object): +class LogSender(object): # pragma: no cover (runs in a different process) """File-like object that writes output to a pipe""" def __init__(self, pipe): self.pipe = pipe @@ -53,7 +48,7 @@ class LogSender(object): if self.pipe: self.pipe.send_bytes(data) - def flush(self, data): + def flush(self): pass def isatty(self): @@ -67,48 +62,32 @@ class Process(object): self.name = name pipes = multiprocessing.Pipe(duplex = False) self._log = LogReceiver(pipes[0]) - self._process = multiprocessing.Process(target = self._tramp, - args = (function, pipes), - name = name) + self._process = multiprocessing.Process( + target = self._tramp, name = name, + args = (function, pipes, parameters)) self._process.daemon = True self._process.start() pipes[1].close() self.start_time = time.time() self.pid = str(uuid.uuid1(self._process.pid or 0)) - def _tramp(self, function, pipes): + def _tramp(self, function, pipes, parameters): # pragma: no cover # Remap stdio in the child before calling function - print "pid:", os.getpid() - print "pipes:", pipes - print "pipes[0].fileno():", pipes[0].fileno() - print "pipes[1].fileno():", pipes[1].fileno() pipes[0].close() logfile = LogSender(pipes[1]) - - saved_stdin = sys.stdin - saved_stdout = sys.stdout - saved_stderr = sys.stderr sys.stdin = open(os.devnull, 'r') sys.stdout = logfile sys.stderr = logfile + function(parameters) - exitcode = 0 - try: - function(self.parameters) - raise Exception("oh no") - except: - traceback.print_exc() - exitcode = 1 - sys.stdin = saved_stdin - sys.stdout = saved_stdout - sys.stderr = saved_stderr - logfile.close() - sys.exit(exitcode) - - def terminate(self, timeout = 3): + def terminate(self, timeout = 1): self._process.join(timeout) if self.alive: self._process.terminate() + self._process.join(timeout) + if self.alive: + return False + return True def clear_log(self): self._log.clear() @@ -122,10 +101,8 @@ class Process(object): return self._process.is_alive() @property - def error(self): - if self._process.exitcode: - return True - return False + def exitcode(self): + return self._process.exitcode class ProcessManager(object): def __init__(self): @@ -141,3 +118,9 @@ class ProcessManager(object): new = Process(name, function, parameters) self.processes[new.pid] = new return new.pid + + def terminate(self, pid): + return self.processes[pid].terminate() + + def remove(self, pid): + del self.processes[pid] diff --git a/nilmrun/server.py b/nilmrun/server.py old mode 100755 new mode 100644 index 493e4e9..bcec06c --- a/nilmrun/server.py +++ b/nilmrun/server.py @@ -9,6 +9,7 @@ import decorator import psutil import traceback import argparse +import time import nilmdb from nilmdb.utils.printf import * @@ -25,6 +26,7 @@ from nilmdb.server.serverutil import ( ) import nilmrun import nilmrun.trainola +import nilmrun.dummyfilter # Add CORS_allow tool cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) @@ -64,7 +66,7 @@ class AppProcess(object): return { "pid": pid, "alive": self.manager[pid].alive, - "error": self.manager[pid].error, + "exitcode": self.manager[pid].exitcode, "name": self.manager[pid].name, "start_time": self.manager[pid].start_time, "parameters": self.manager[pid].parameters, @@ -75,6 +77,8 @@ class AppProcess(object): @cherrypy.expose @cherrypy.tools.json_out() def status(self, pid, clear = False): + """Return status about a process. If clear = True, also clear + the log.""" if pid not in self.manager: raise cherrypy.HTTPError("404 Not Found", "No such PID") status = self.process_status(pid) @@ -86,21 +90,24 @@ class AppProcess(object): @cherrypy.expose @cherrypy.tools.json_out() def list(self): + """Return a list of processes in the manager.""" return list(self.manager) - # /process/kill + # /process/remove @cherrypy.expose @cherrypy.tools.json_in() @cherrypy.tools.json_out() @cherrypy.tools.CORS_allow(methods = ["POST"]) - def kill(self, pid): + def remove(self, pid): + """Remove a process from the manager, killing it if necessary""" if pid not in self.manager: raise cherrypy.HTTPError("404 Not Found", "No such PID") if not self.manager.terminate(pid): raise cherrypy.HTTPError("503 Service Unavailable", "Failed to stop process") status = self.process_status(pid) - manager.remove(pid) + self.manager.remove(pid) + return status class AppFilter(object): @@ -116,6 +123,14 @@ class AppFilter(object): def trainola(self, data): return self.manager.run("trainola", nilmrun.trainola.trainola, data) + # /filter/dummy + @cherrypy.expose + @cherrypy.tools.json_in() + @cherrypy.tools.json_out() + @exception_to_httperror(KeyError, ValueError) + @cherrypy.tools.CORS_allow(methods = ["POST"]) + def dummy(self, count): + return self.manager.run("dummy", nilmrun.dummyfilter.dummy, int(count)) class Server(object): def __init__(self, host = '127.0.0.1', port = 8080, diff --git a/nilmrun/trainola.py b/nilmrun/trainola.py old mode 100755 new mode 100644 diff --git a/setup.cfg b/setup.cfg index 4bc75cb..c8fbcb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ test = nosetests nocapture=1 # Comment this out to see CherryPy logs on failure: nologcapture=1 -#with-coverage=1 +with-coverage=1 cover-inclusive=1 cover-package=nilmrun cover-erase=1 diff --git a/tests/test_client.py b/tests/test_client.py index 8613e2e..a7d5982 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,7 +2,7 @@ import nilmrun.server -from nilmdb.client.httpclient import HTTPClient, ClientError +from nilmdb.client.httpclient import HTTPClient, ClientError, ServerError from nilmdb.utils.printf import * @@ -33,8 +33,6 @@ testurl = "http://localhost:32181/" def setup_module(): global test_server - print dir(nilmrun) - # Start web app on a custom port test_server = nilmrun.server.Server(host = "127.0.0.1", port = 32181, @@ -68,10 +66,100 @@ class TestClient(object): client.get("/process/status", { "pid": 12345 }) in_("No such PID", str(e.exception)) with assert_raises(ClientError): - client.get("/process/kill", { "pid": 12345 }) + client.get("/process/remove", { "pid": 12345 }) in_("No such PID", str(e.exception)) - def test_client_03_trainola(self): + def test_client_03_process_basic(self): + client = HTTPClient(baseurl = testurl, post_json = True) + + # start dummy filter + pid = client.post("/filter/dummy", { "count": 30 }) + eq_(client.get("/process/list"), [pid]) + time.sleep(1) + + # Verify that status looks OK + status = client.get("/process/status", { "pid": pid, "clear": True }) + for x in [ "pid", "alive", "exitcode", "name", + "start_time", "parameters", "log" ]: + in_(x, status) + in_("dummy 0\ndummy 1\ndummy 2\ndummy 3\n", status["log"]) + eq_(status["alive"], True) + eq_(status["exitcode"], None) + + # Check that the log got cleared + status = client.get("/process/status", { "pid": pid }) + nin_("dummy 0\ndummy 1\ndummy 2\ndummy 3\n", status["log"]) + + # See that it ended properly + start = time.time() + while status["alive"] == True and (time.time() - start) < 5: + status = client.get("/process/status", { "pid": pid }) + in_("dummy 27\ndummy 28\ndummy 29\n", status["log"]) + eq_(status["alive"], False) + eq_(status["exitcode"], 0) + + # Remove it + killstatus = client.post("/process/remove", { "pid": pid }) + eq_(status, killstatus) + eq_(client.get("/process/list"), []) + with assert_raises(ClientError) as e: + client.post("/process/remove", { "pid": pid }) + in_("No such PID", str(e.exception)) + + def test_client_04_process_terminate(self): + client = HTTPClient(baseurl = testurl, post_json = True) + + # Trigger exception in filter + pid = client.post("/filter/dummy", { "count": -1 }) + time.sleep(0.5) + status = client.get("/process/status", { "pid": pid }) + eq_(status["alive"], False) + eq_(status["exitcode"], 1) + in_("Exception: test exception", status["log"]) + client.post("/process/remove", { "pid": pid }) + + # Kill a running filter by removing it early + newpid = client.post("/filter/dummy", { "count": 30 }) + ne_(newpid, pid) + time.sleep(0.5) + status = client.post("/process/remove", { "pid": newpid }) + eq_(status["alive"], False) + ne_(status["exitcode"], 0) + + # No more + eq_(client.get("/process/list"), []) + + # Try to remove a running filter that ignored SIGTERM + # (can't be killed) + pid = client.post("/filter/dummy", { "count": 0 }) + with assert_raises(ServerError) as e: + status = client.post("/process/remove", { "pid": pid }) + in_("503 Service Unavailable", str(e.exception)) + in_("Failed to stop process", str(e.exception)) + + # Wait for it to die, then remove it + start = time.time() + while (time.time() - start) < 5: + status = client.get("/process/status", { "pid": pid }) + if status["alive"] == False: + break + eq_(status["alive"], False) + in_("dummy 39\n", status["log"]) + eq_(status["exitcode"], 0) + + def test_client_05_trainola_simple(self): + client = HTTPClient(baseurl = testurl, post_json = True) + pid = client.post("/filter/trainola", { "data": {} }) + start = time.time() + while (time.time() - start) < 5: + status = client.get("/process/status", { "pid": pid }) + if status["alive"] == False: + break + eq_(status["alive"], False) + ne_(status["exitcode"], 0) + + @unittest.skip("needs a running nilmdb") + def test_client_06_trainola(self): client = HTTPClient(baseurl = testurl, post_json = True) data = { "url": "http://bucket.mit.edu/nilmdb", @@ -102,15 +190,26 @@ class TestClient(object): } ] } - client.post("/filter/trainola", { "data": data }) - pid = client.get("/process/list")[0] - eq_(client.get("/process/list"), [pid]) - while True: + # start trainola + pid = client.post("/filter/trainola", { "data": data }) + + # wait for it to finish + for i in range(60): time.sleep(1) - status = client.get("/process/status", { "pid": pid }) - pprint.pprint(status) - print status["alive"] + if i == 2: + status = client.get("/process/status", { "pid": pid, + "clear": True }) + in_("Loading stream data", status['log']) + elif i == 3: + status = client.get("/process/status", { "pid": pid }) + nin_("Loading stream data", status['log']) + else: + status = client.get("/process/status", { "pid": pid }) if status["alive"] == False: break - pprint.pprint(status) + else: + client.post("/process/remove", {"pid": pid }) + raise AssertionError("took too long") + if i < 3: + raise AssertionError("too fast?") diff --git a/tests/testutil/helpers.py b/tests/testutil/helpers.py index 43a57f3..b736bd6 100644 --- a/tests/testutil/helpers.py +++ b/tests/testutil/helpers.py @@ -18,6 +18,10 @@ def in_(a, b): if a not in b: raise AssertionError("%s not in %s" % (myrepr(a), myrepr(b))) +def nin_(a, b): + if a in b: + raise AssertionError("unexpected %s in %s" % (myrepr(a), myrepr(b))) + def in2_(a1, a2, b): if a1 not in b and a2 not in b: raise AssertionError("(%s or %s) not in %s" % (myrepr(a1), myrepr(a2),