diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..b12e882 Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc index d2f1163..6de8124 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,4 +7,4 @@ exclude_lines = pragma: no cover if 0: -omit = scripts,src/_version.py +omit = scripts,nilmrun/_version.py diff --git a/Makefile b/Makefile index 1af6400..284bdf5 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,15 @@ -URL="http://localhost/nilmdb" +# By default, run the tests. +all: test -all: -ifeq ($(INSIDE_EMACS), t) - @make test -else - @echo "Try 'make install'" -endif - -test: - src/trainola.py data.js +test2: + nilmrun/trainola.py data.js version: python setup.py version +build: + python setup.py build_ext --inplace + dist: sdist sdist: python setup.py sdist @@ -23,11 +20,29 @@ install: develop: python setup.py develop +docs: + make -C docs + +lint: + pylint --rcfile=.pylintrc nilmdb + +test: +ifeq ($(INSIDE_EMACS), t) +# Use the slightly more flexible script + python setup.py build_ext --inplace + python tests/runtests.py +else +# Let setup.py check dependencies, build stuff, and run the test + python setup.py nosetests +endif + clean:: + rm -f .coverage find . -name '*pyc' | xargs rm -f rm -rf nilmtools.egg-info/ build/ MANIFEST.in + make -C docs clean gitclean:: git clean -dXf -.PHONY: all test version dist sdist install clean gitclean +.PHONY: all version dist sdist install docs lint test clean gitclean diff --git a/src/__init__.py b/nilmrun/__init__.py similarity index 76% rename from src/__init__.py rename to nilmrun/__init__.py index 74f4e66..fe5308a 100644 --- a/src/__init__.py +++ b/nilmrun/__init__.py @@ -1,3 +1,4 @@ +import nilmrun.threadmanager from ._version import get_versions __version__ = get_versions()['version'] diff --git a/src/_version.py b/nilmrun/_version.py similarity index 99% rename from src/_version.py rename to nilmrun/_version.py index 38b8373..a42961f 100644 --- a/src/_version.py +++ b/nilmrun/_version.py @@ -181,7 +181,7 @@ def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False) tag_prefix = "nilmrun-" parentdir_prefix = "nilmrun-" -versionfile_source = "src/_version.py" +versionfile_source = "nilmrun/_version.py" def get_versions(default={"version": "unknown", "full": ""}, verbose=False): variables = { "refnames": git_refnames, "full": git_full } diff --git a/src/server.py b/nilmrun/server.py similarity index 93% rename from src/server.py rename to nilmrun/server.py index bf4ec44..2f115cf 100755 --- a/src/server.py +++ b/nilmrun/server.py @@ -41,7 +41,7 @@ class App(object): def index(self): cherrypy.response.headers['Content-Type'] = 'text/plain' msg = sprintf("This is NilmRun version %s, running on host %s.\n", - nilmdb.__version__, socket.getfqdn()) + nilmrun.__version__, socket.getfqdn()) return msg # /favicon.ico @@ -81,6 +81,12 @@ class AppThread(object): self.manager[pid].clear_log() return status + # /thread/list + @cherrypy.expose + @cherrypy.tools.json_out() + def list(self): + return list(self.manager) + # /thread/kill @cherrypy.expose @cherrypy.tools.json_in() @@ -97,14 +103,17 @@ class AppThread(object): class AppFilter(object): + def __init__(self, manager): + self.manager = manager + # /filter/trainola @cherrypy.expose @cherrypy.tools.json_in() @cherrypy.tools.json_out() - @exception_to_httperror(KeyError, ValueError, NilmDBError) + @exception_to_httperror(KeyError, ValueError) @cherrypy.tools.CORS_allow(methods = ["POST"]) def trainola(self, data): - return nilmrun.trainola.trainola(data) + return self.manager.run("trainola", nilmrun.trainola.trainola, data) class Server(object): def __init__(self, host = '127.0.0.1', port = 8080, @@ -156,9 +165,10 @@ class Server(object): cherrypy._cperror._ie_friendly_error_sizes = {} # Build up the application and mount it + manager = nilmrun.threadmanager.ThreadManager() root = App() - root.thread = AppThread() - root.filter = AppFilter() + root.thread = AppThread(manager) + root.filter = AppFilter(manager) cherrypy.tree.apps = {} cherrypy.tree.mount(root, basepath, config = { "/" : app_config }) diff --git a/nilmrun/threadmanager.py b/nilmrun/threadmanager.py new file mode 100644 index 0000000..1cc7f1d --- /dev/null +++ b/nilmrun/threadmanager.py @@ -0,0 +1,46 @@ +#!/usr/bin/python + +from nilmdb.utils.printf import * + +import threading + +class Thread(object): + def __init__(self, name, function, parameters): + self.parameters = parameters + self.start_time = None + self.log = '' + self._terminate = threading.Event() + self._thread = threading.Thread(target = function, + name = name, + args = ( parameters, + self._terminate )) + self._thread.daemon = True + self._thread.start() + + def terminate(self, timeout = 60): + self._terminate.set() + self._thread.join(timeout = 60) + + @property + def pid(self): + return self._thread.ident + + @property + def name(self): + return self._thread.name + + @property + def alive(self): + return self._thread.is_alive() + +class ThreadManager(object): + def __init__(self): + self.threads = {} + + def __iter__(self): + return iter(self.threads.keys()) + + def run(self, name, function, parameters): + new = Thread(name, function, parameters) + self.threads[new.pid] = new + return new.pid diff --git a/src/trainola.py b/nilmrun/trainola.py similarity index 100% rename from src/trainola.py rename to nilmrun/trainola.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c8fbcb4 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,22 @@ +[aliases] +test = nosetests + +[nosetests] +# Note: values must be set to 1, and have no comments on the same line, +# for "python setup.py nosetests" to work correctly. +nocapture=1 +# Comment this out to see CherryPy logs on failure: +nologcapture=1 +with-coverage=1 +cover-inclusive=1 +cover-package=nilmrun +cover-erase=1 +# this works, puts html output in cover/ dir: +# cover-html=1 +# need nose 1.1.3 for this: +# cover-branches=1 +#debug=nose +#debug-log=nose.log +stop=1 +verbosity=2 +tests=tests diff --git a/setup.py b/setup.py index 37f89fb..16056c7 100755 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ except ImportError: # Versioneer manages version numbers from git tags. # https://github.com/warner/python-versioneer import versioneer -versioneer.versionfile_source = 'src/_version.py' +versioneer.versionfile_source = 'nilmrun/_version.py' versioneer.versionfile_build = 'nilmrun/_version.py' versioneer.tag_prefix = 'nilmrun-' versioneer.parentdir_prefix = 'nilmrun-' @@ -69,7 +69,7 @@ setup(name='nilmrun', packages = [ 'nilmrun', 'nilmrun.scripts', ], - package_dir = { 'nilmrun': 'src', + package_dir = { 'nilmrun': 'nilmrun', 'nilmrun.scripts': 'scripts', }, entry_points = { diff --git a/src/threadmanager.py b/src/threadmanager.py deleted file mode 100644 index 29b6368..0000000 --- a/src/threadmanager.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/python - -from nilmdb.utils.printf import * - -import threading - -class ThreadManager(object): - def __init__(self): - self.threads = {} - - diff --git a/tests/runtests.py b/tests/runtests.py new file mode 100755 index 0000000..8c25399 --- /dev/null +++ b/tests/runtests.py @@ -0,0 +1,49 @@ +#!/usr/bin/python + +import nose +import os +import sys +import glob +from collections import OrderedDict + +# Change into parent dir +os.chdir(os.path.dirname(os.path.realpath(__file__)) + "/..") + +class JimOrderPlugin(nose.plugins.Plugin): + """When searching for tests and encountering a directory that + contains a 'test.order' file, run tests listed in that file, in the + order that they're listed. Globs are OK in that file and duplicates + are removed.""" + name = 'jimorder' + score = 10000 + + def prepareTestLoader(self, loader): + def wrap(func): + def wrapper(name, *args, **kwargs): + addr = nose.selector.TestAddress( + name, workingDir=loader.workingDir) + try: + order = os.path.join(addr.filename, "test.order") + except Exception: + order = None + if order and os.path.exists(order): + files = [] + for line in open(order): + line = line.split('#')[0].strip() + if not line: + continue + fn = os.path.join(addr.filename, line.strip()) + files.extend(sorted(glob.glob(fn)) or [fn]) + files = list(OrderedDict.fromkeys(files)) + tests = [ wrapper(fn, *args, **kwargs) for fn in files ] + return loader.suiteClass(tests) + return func(name, *args, **kwargs) + return wrapper + loader.loadTestsFromName = wrap(loader.loadTestsFromName) + return loader + +# Use setup.cfg for most of the test configuration. Adding +# --with-jimorder here means that a normal "nosetests" run will +# still work, it just won't support test.order. +nose.main(addplugins = [ JimOrderPlugin() ], + argv = sys.argv + ["--with-jimorder"]) diff --git a/tests/test.order b/tests/test.order new file mode 100644 index 0000000..e24c566 --- /dev/null +++ b/tests/test.order @@ -0,0 +1,3 @@ +test_client.py + +test_*.py diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..b8180db --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import nilmrun.server + +from nilmdb.client.httpclient import HTTPClient, ClientError + +from nilmdb.utils.printf import * + +from nose.plugins.skip import SkipTest +from nose.tools import * +from nose.tools import assert_raises + +import itertools +import distutils.version +import os +import sys +import threading +import cStringIO +import simplejson as json +import unittest +import warnings +import time +import re +import urllib2 +from urllib2 import urlopen, HTTPError +import requests + +from testutil.helpers import * + +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, + force_traceback = True) + test_server.start(blocking = False) + +def teardown_module(): + global test_server + # Close web app + test_server.stop() + +class TestClient(object): + + def test_client_01_basic(self): + client = HTTPClient(baseurl = testurl) + version = client.get("/version") + eq_(distutils.version.LooseVersion(version), + distutils.version.LooseVersion(nilmrun.__version__)) + + in_("This is NilmRun", client.get("/")) + + with assert_raises(ClientError): + client.get("/favicon.ico") + + def test_client_02_manager(self): + client = HTTPClient(baseurl = testurl) + + eq_(client.get("/thread/list"), []) + + with assert_raises(ClientError): + client.get("/thread/status", { "pid": 12345 }) + with assert_raises(ClientError): + client.get("/thread/kill", { "pid": 12345 }) + + def test_client_03_trainola(self): + client = HTTPClient(baseurl = testurl) + + data = { "url": "http://bucket.mit.edu/nilmdb", + "stream": "/sharon/prep-a", + "start": 1366111383280463, + "end": 1366126163457797, + "columns": [ { "name": "P1", "index": 0 }, + { "name": "Q1", "index": 1 }, + { "name": "P3", "index": 2 } ], + "exemplars": [ + { "name": "Boiler Pump ON", + "url": "http://bucket.mit.edu/nilmdb", + "stream": "/sharon/prep-a", + "start": 1366260494269078, + "end": 1366260608185031, + "columns": [ { "name": "P1", "index": 0 }, + { "name": "Q1", "index": 1 } + ] + }, + { "name": "Boiler Pump OFF", + "url": "http://bucket.mit.edu/nilmdb", + "stream": "/sharon/prep-a", + "start": 1366260864215764, + "end": 1366260870882998, + "columns": [ { "name": "P1", "index": 0 }, + { "name": "Q1", "index": 1 } + ] + } + ] + } + client.post("/filter/trainola", { "data": data }) + diff --git a/tests/testutil/__init__.py b/tests/testutil/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/tests/testutil/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/tests/testutil/helpers.py b/tests/testutil/helpers.py new file mode 100644 index 0000000..43a57f3 --- /dev/null +++ b/tests/testutil/helpers.py @@ -0,0 +1,37 @@ +# Just some helpers for test functions + +def myrepr(x): + if isinstance(x, basestring): + return '"' + x + '"' + else: + return repr(x) + +def eq_(a, b): + if not a == b: + raise AssertionError("%s != %s" % (myrepr(a), myrepr(b))) + +def lt_(a, b): + if not a < b: + raise AssertionError("%s is not less than %s" % (myrepr(a), myrepr(b))) + +def in_(a, b): + if a not in b: + raise AssertionError("%s not 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), + myrepr(b))) + +def ne_(a, b): + if not a != b: + raise AssertionError("unexpected %s == %s" % (myrepr(a), myrepr(b))) + +def lines_(a, n): + l = a.count('\n') + if not l == n: + if len(a) > 5000: + a = a[0:5000] + " ... truncated" + raise AssertionError("wanted %d lines, got %d in output: '%s'" + % (n, l, a)) +