Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

100% test coverage #1

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.coverage
build/
debian/debhelper-build-stamp
debian/files
debian/*.log
debian/*.debhelper
debian/*.substvars
debian/ips-reverseproxy-common/
nmosreverseproxy.egg-info/
*.py,cover
*.pyc
virtpython
28 changes: 25 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ PYTHON=`which python`
DESTDIR=/
PROJECT=reverseproxy

TEST_DEPS=\
mock \
gevent

VENV=virtpython
VENV_ACTIVATE=$(VENV)/bin/activate
VENV_MODULE_DIR=$(VENV)/lib/python2.7/site-packages
VENV_TEST_DEPS=$(addprefix $(VENV_MODULE_DIR)/,$(TEST_DEPS))

all:
@echo "make source - Create source package"
@echo "make source - Create source package"
@echo "make install - Install on local system (only during development)"
@echo "make deb - Generate a deb package - for local testing"
@echo "make clean - Get rid of scratch and byte files"
@echo "make deb - Generate a deb package - for local testing"
@echo "make clean - Get rid of scratch and byte files"
@echo "make test - Tests are nice"

source:
$(PYTHON) setup.py sdist $(COMPILE)
Expand All @@ -22,3 +32,15 @@ clean:
dh_clean
rm -rf build/ MANIFEST
find . -name '*.pyc' -delete
rm -rf $(VENV)

$(VENV):
virtualenv --system-site-packages $@

$(VENV_TEST_DEPS): $(VENV)
. $(VENV_ACTIVATE); pip install $(@F)

test: $(VENV_TEST_DEPS)
. $(VENV_ACTIVATE); $(PYTHON) -m unittest discover -s .

.PHONY: test clean deb install source all
6 changes: 3 additions & 3 deletions nmosreverseproxy/proxylisting.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ProxyListingAPI(WebAPI):
def __init__(self):
super(ProxyListingAPI, self).__init__()

@resource_route("/")
@route("/")
def base(self):
alias_list = ["x-ipstudio/", "x-nmos/"]
for conffile in listdir(ALIAS_SITES):
Expand All @@ -41,11 +41,11 @@ def base(self):
alias_list.sort()
return alias_list

@resource_route("/x-ipstudio/")
@route("/x-ipstudio/")
def x_ipstudio(self):
return self.get_apis("x-ipstudio")

@resource_route("/x-nmos/")
@route("/x-nmos/")
def x_nmos(self):
return self.get_apis("x-nmos")

Expand Down
4 changes: 2 additions & 2 deletions nmosreverseproxy/proxylistingservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
HOST = "127.0.0.1"
PORT = 12344

class ProxyListingService:
class ProxyListingService(object):
def __init__(self):
self.running = False

Expand Down Expand Up @@ -58,7 +58,7 @@ def sig_handler(self, sig, frame):
self.stop()


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover

service = ProxyListingService()
service.start()
Expand Down
Empty file added tests/__init__.py
Empty file.
134 changes: 134 additions & 0 deletions tests/test_proxylisting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/python

# Copyright 2017 British Broadcasting Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest
import mock
import os

class WebAPIStub(object):
"""This is used to replace the WebAPI class so that ProxyListingAPI can inherit from it"""
def __init__(self):
super(WebAPIStub, self).__setattr__('mock', mock.MagicMock(name='WebAPI'))

def __getattr__(self, name):
return getattr(self.mock, name)

def __setattr__(self, name, value):
return setattr(self.mock, name, value)

route = mock.MagicMock(name="route")
def _route(path):
"""This ensures that a unique mock is accessible for each call to resource_route"""
m = getattr(route, path)
m.side_effect = lambda f : f
return m
route.side_effect = _route

_orig_getenv = os.getenv
def _getenv(*args, **kwargs):
"""This allows us to substitue known values for the environment variables used by the code."""
if args[0] == "PROXYLISTING_ALIAS_SITES":
return "/ALIAS_SITES/"
elif args[0] == "PROXYLISTING_PROXY_SITES":
return "/PROXY_SITES/"
else:
return _orig_getenv(*args, **kwargs)

with mock.patch('os.getenv', side_effect=_getenv) as getenv:
with mock.patch('nmoscommon.webapi.WebAPI', WebAPIStub):
with mock.patch('nmoscommon.webapi.route', route):
from nmosreverseproxy.proxylisting import *

class TestProxyListingAPI(unittest.TestCase):
def setUp(self):
self.UUT = ProxyListingAPI()
self.routes = {}
for (name, args, kwargs) in route.mock_calls:
if name != "":
self.routes[name] = args[0]

def test_init(self):
"""Just check that routes have been resgistered for the necessary paths."""
self.assertIn('/', self.routes)
self.assertIn('/x-ipstudio/', self.routes)
self.assertIn('/x-nmos/', self.routes)
self.assertIn(mock.call("PROXYLISTING_ALIAS_SITES", "/etc/apache2/sites-enabled/"), getenv.mock_calls)
self.assertIn(mock.call("PROXYLISTING_PROXY_SITES", "/etc/apache2/sites-available/"), getenv.mock_calls)

@mock.patch('__builtin__.open')
@mock.patch('nmosreverseproxy.proxylisting.isfile')
@mock.patch('nmosreverseproxy.proxylisting.listdir')
def assert_route_call_behaves_as_expected(self, path, listdir, isfile, _open, alias_sites={}, proxy_sites={}, expected=None):
"""Test the resource registered for path '/' with the given files in the alias directory."""
f = self.routes[path]

def _listdir(dir):
if dir == "/ALIAS_SITES/":
return [ key for key in alias_sites ]
else:
return [ key for key in proxy_sites ]
listdir.side_effect = _listdir
isfile.side_effect = lambda key : (key[:13] == "/ALIAS_SITES/" and alias_sites[key[13:]] is not None) or (key[:13] == "/PROXY_SITES/" and proxy_sites[key[13:]] is not None)
open.side_effect = lambda key : mock.MagicMock(__enter__=mock.MagicMock(return_value=alias_sites[key[13:]] if key[:13] == "/ALIAS_SITES/" else proxy_sites[key[13:]]))

ret_list = f(self.UUT)
self.assertListEqual(ret_list, expected)

def test_base_with_no_alias_sites(self):
"""With no files in the alias directory only the default sites exist"""
self.assert_route_call_behaves_as_expected('/', alias_sites={}, expected=['x-ipstudio/', 'x-nmos/'])

def test_base_with_alias_sites(self):
"""Alias sites will add extra entries here, and that is all."""
self.assert_route_call_behaves_as_expected('/', alias_sites={ "notafile" : None, "notaconffile" : [], "dummy.conf" : [ "Alias /dummy/path/1/",
"Alias /dimmy/path/2/"] },
expected=[ "dimmy/", "dummy/", 'x-ipstudio/', 'x-nmos/' ])

def test_x_ipstudio_with_no_proxy_sites(self):
"""With no files in the proxy directory only no sites exist"""
self.assert_route_call_behaves_as_expected('/x-ipstudio/', expected=[])

def test_x_ipstudio_with_proxy_sites(self):
"""With no files in the proxy directory only no sites exist"""
self.assert_route_call_behaves_as_expected('/x-ipstudio/',
proxy_sites={
"notafile" : None,
"notaconffile" : [ "<Location /BAD/UNPLEASANT/" ],
"dummy.conf" : [ "<Location /ANGRY/UNMUTUAL/" ],
"ips-api-dummy.conf" : [ "<Location /x-ipstudio/GOOD/",
"<Location /x-ipstudio/VGOOD/"],
"nmos-api-dummy.conf" : [ "<Location /x-nmos/NICE/",
"<Location /x-nmos/VNICE/"]
},
expected=[ 'GOOD/', "VGOOD/" ])

def test_x_nmos_with_no_proxy_sites(self):
"""With no files in the proxy directory only no sites exist"""
self.assert_route_call_behaves_as_expected('/x-nmos/', expected=[])

def test_x_nmos_with_proxy_sites(self):
"""With no files in the proxy directory only no sites exist"""
self.assert_route_call_behaves_as_expected('/x-nmos/',
proxy_sites={
"notafile" : None,
"notaconffile" : [ "<Location /BAD/UNPLEASANT/" ],
"dummy.conf" : [ "<Location /ANGRY/UNMUTUAL/" ],
"ips-api-dummy.conf" : [ "<Location /x-ipstudio/GOOD/",
"<Location /x-ipstudio/VGOOD/"],
"nmos-api-dummy.conf" : [ "<Location /x-nmos/NICE/",
"<Location /x-nmos/VNICE/"]
},
expected=[ 'NICE/', "VNICE/" ])
88 changes: 88 additions & 0 deletions tests/test_proxylistingservice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/python

# Copyright 2017 British Broadcasting Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest
import mock
from signal import SIGINT

class ProxyListingService(unittest.TestCase):
def setUp(self):
from nmosreverseproxy.proxylistingservice import ProxyListingService
self.UUT = ProxyListingService()

def test_init(self):
self.assertFalse(self.UUT.running)

@mock.patch('nmosreverseproxy.proxylistingservice.HttpServer')
@mock.patch('signal.signal')
@mock.patch('time.sleep')
def test_run(self, sleep, _signal, HttpServer):
"""The run method should just start up a webserver and then wait forever"""
from nmosreverseproxy.proxylisting import ProxyListingAPI

HttpServer.return_value.started.is_set.side_effect = [ False, False, True ]
HttpServer.return_value.failed = None
sleep.side_effect = lambda _ : self.UUT.stop()

self.UUT.run()

_signal.assert_called_once_with(SIGINT, mock.ANY)
self.sighandle = _signal.call_args[0][1]
HttpServer.assert_called_once_with(ProxyListingAPI, 12344, "127.0.0.1")
HttpServer.return_value.start.assert_called_once_with()
HttpServer.return_value.started.wait.mock_calls = [ mock.call() for _ in range(0,3) ]

@mock.patch('nmosreverseproxy.proxylistingservice.HttpServer')
@mock.patch('signal.signal')
@mock.patch('time.sleep')
def test_run_bails_when_http_server_fails_to_start(self, sleep, _signal, HttpServer):
from nmosreverseproxy.proxylisting import ProxyListingAPI

expected = Exception()

HttpServer.return_value.started.is_set.side_effect = [ False, False, True ]
HttpServer.return_value.failed = expected
sleep.side_effect = lambda _ : self.UUT.stop()

with self.assertRaises(Exception) as e:
self.UUT.run()

_signal.assert_called_once_with(SIGINT, mock.ANY)
self.sighandle = _signal.call_args[0][1]
HttpServer.assert_called_once_with(ProxyListingAPI, 12344, "127.0.0.1")
HttpServer.return_value.start.assert_called_once_with()
HttpServer.return_value.started.wait.mock_calls = [ mock.call() for _ in range(0,3) ]

self.assertEqual(e.exception, expected)

@mock.patch('nmosreverseproxy.proxylistingservice.HttpServer')
@mock.patch('signal.signal')
@mock.patch('time.sleep')
def test_signal_handler_set_by_run_calls_stop(self, sleep, _signal, HttpServer):
"""The run method should just start up a webserver and then wait forever"""
from nmosreverseproxy.proxylisting import ProxyListingAPI

HttpServer.return_value.started.is_set.side_effect = [ False, False, True ]
HttpServer.return_value.failed = None
sleep.side_effect = lambda _ : self.UUT.stop()

self.UUT.run()

sighandle = _signal.call_args[0][1]

with mock.patch.object(self.UUT, 'stop') as _stop:
sighandle(mock.sentinel.sig, mock.sentinel.frame)
_stop.assert_called_once_with()