From 863377441bdc70885d1bad80bbbd9c6a9a3646a7 Mon Sep 17 00:00:00 2001 From: Andrey Zelenkov Date: Tue, 21 Nov 2017 20:51:21 +0300 Subject: [PATCH] Tests: added basic infrastructure. --- test/run.py | 14 ++++ test/test_basic.py | 163 +++++++++++++++++++++++++++++++++++++ test/test_configuration.py | 136 +++++++++++++++++++++++++++++++ test/unit.py | 112 +++++++++++++++++++++++++ 4 files changed, 425 insertions(+) create mode 100755 test/run.py create mode 100644 test/test_basic.py create mode 100644 test/test_configuration.py create mode 100644 test/unit.py diff --git a/test/run.py b/test/run.py new file mode 100755 index 00000000..be76a4fb --- /dev/null +++ b/test/run.py @@ -0,0 +1,14 @@ +#!/usr/bin/python3 + +import unittest +import os + +loader = unittest.TestLoader() +suite = unittest.TestSuite() + +this_dir = os.path.dirname(__file__) +tests = loader.discover(start_dir=this_dir) +suite.addTests(tests) + +runner = unittest.TextTestRunner(verbosity=3) +result = runner.run(suite) diff --git a/test/test_basic.py b/test/test_basic.py new file mode 100644 index 00000000..6be3801b --- /dev/null +++ b/test/test_basic.py @@ -0,0 +1,163 @@ +import unit +import unittest + +class TestUnitBasic(unit.TestUnitControl): + + def test_get(self): + resp = self.get() + self.assertEqual(resp, {'listeners': {}, 'applications': {}}, 'empty') + self.assertEqual(self.get('/listeners'), {}, 'empty listeners prefix') + self.assertEqual(self.get('/applications'), {}, + 'empty applications prefix') + + self.put('/applications', """ + { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """) + + resp = self.get() + + self.assertEqual(resp['listeners'], {}, 'python empty listeners') + self.assertEqual(resp['applications'], + { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + }, + 'python applications') + + self.assertEqual(self.get('/applications'), + { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module":"wsgi" + } + }, + 'python applications prefix') + + self.assertEqual(self.get('/applications/app'), + { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + }, + 'python applications prefix 2') + + self.assertEqual(self.get('/applications/app/type'), 'python', + 'python applications type') + self.assertEqual(self.get('/applications/app/workers'), 1, + 'python applications workers') + + self.put('/listeners', '{"*:8080":{"application":"app"}}') + + self.assertEqual(self.get()['listeners'], + {"*:8080":{"application":"app"}}, 'python listeners') + self.assertEqual(self.get('/listeners'), + {"*:8080":{"application":"app"}}, 'python listeners prefix') + self.assertEqual(self.get('/listeners/*:8080'), + {"application":"app"}, 'python listeners prefix 2') + self.assertEqual(self.get('/listeners/*:8080/application'), 'app', + 'python listeners application') + + def test_put(self): + self.put('/', """ + { + "listeners": { + "*:8080": { + "application": "app" + } + }, + "applications": { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + } + """) + + resp = self.get() + + self.assertEqual(resp['listeners'], {"*:8080":{"application":"app"}}, + 'put listeners') + + self.assertEqual(resp['applications'], + { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + }, + 'put applications') + + self.put('/listeners', '{"*:8081":{"application":"app"}}') + self.assertEqual(self.get('/listeners'), + {"*:8081": {"application":"app"}}, 'put listeners prefix') + + self.put('/listeners/*:8080', '{"application":"app"}') + + self.assertEqual(self.get('/listeners'), + { + "*:8080": { + "application": "app" + }, + "*:8081": { + "application": "app" + } + }, + 'put listeners prefix 3') + + self.put('/applications/app/workers', '30') + self.assertEqual(self.get('/applications/app/workers'), 30, + 'put applications workers') + + self.put('/applications/app/path', '"/www"') + self.assertEqual(self.get('/applications/app/path'), '/www', + 'put applications path') + + def test_delete(self): + self.put('/', """ + { + "listeners": { + "*:8080": { + "application": "app" + } + }, + "applications": { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + } + """) + + self.assertIn('error', self.delete('/applications/app'), + 'delete app before listener') + self.assertIn('success', self.delete('/listeners/*:8080'), + 'delete listener') + self.assertIn('success', self.delete('/applications/app'), + 'delete app after listener') + self.assertIn('error', self.delete('/applications/app'), + 'delete app again') + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_configuration.py b/test/test_configuration.py new file mode 100644 index 00000000..863250cd --- /dev/null +++ b/test/test_configuration.py @@ -0,0 +1,136 @@ +import unit +import unittest + +class TestUnitConfiguration(unit.TestUnitControl): + + def test_json_applications(self): + self.assertIn('error', self.put('/applications', '"{}"'), + 'applications string') + self.assertIn('error', self.put('/applications', '{'), + 'applications miss brace') + + self.assertIn('error', self.put('/applications', """ + { + app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """), 'applications miss quote') + + self.assertIn('error', self.put('/applications', """ + { + "app" { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """), 'applications miss colon') + + self.assertIn('error', self.put('/applications', """ + { + "app": { + "type": "python" + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """), 'applications miss comma') + + self.assertIn('success', self.put('/applications', b'{ \n\r\t}'), + 'skip space') + + self.assertIn('success', self.put('/applications', """ + { + "app": { + "type": "python", + "workers": 1, + "path": "../app", + "module": "wsgi" + } + } + """), 'relative path') + + self.assertIn('success', self.put('/applications', b""" + { + "ap\u0070": { + "type": "\u0070ython", + "workers": 1, + "path": "\u002Fapp", + "module": "wsgi" + } + } + """), 'unicode') + + self.assertIn('success', self.put('/applications', """ + { + "приложение": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """), 'unicode 2') + + self.assertIn('error', self.put('/applications', b""" + { + "app": { + "type": "python", + "workers": \u0031, + "path": "/app", + "module": "wsgi" + } + } + """), 'unicode number') + + def test_json_listeners(self): + self.assertIn('error', self.put('/listeners', + '{"*:8080":{"application":"app"}}'), 'listeners no app') + + self.put('/applications', """ + { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """) + + self.assertIn('success', self.put('/listeners', + '{"*:8080":{"application":"app"}}'), 'listeners wildcard') + self.assertIn('success', self.put('/listeners', + '{"127.0.0.1:8081":{"application":"app"}}'), 'listeners explicit') + self.assertIn('success', self.put('/listeners', + '{"[::1]:8082":{"application":"app"}}'), 'listeners explicit ipv6') + self.assertIn('error', self.put('/listeners', + '{"127.0.0.1":{"application":"app"}}'), 'listeners no port') + + @unittest.skip("TODO") + def test_broken(self): + self.assertIn('error', self.put('/', '00'), 'leading zero') + self.assertIn('error', self.put('/listeners', '{"*:8080":{}}'), + 'listener empty') + self.assertIn('error', self.put('/applications', '"type":"python"'), + 'application type only') + + self.assertIn('error', self.put('/applications', """ + { + "app": { + "type": "python", + "workers": 1, + "path": "/app", + "module": "wsgi" + } + } + """), 'negative workers') + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit.py b/test/unit.py new file mode 100644 index 00000000..224d026e --- /dev/null +++ b/test/unit.py @@ -0,0 +1,112 @@ +import os +import re +import sys +import json +import time +import shutil +import socket +import tempfile +import unittest +import subprocess + +class TestUnit(unittest.TestCase): + + def setUp(self): + self.testdir = tempfile.mkdtemp(prefix='unit-test-') + + os.mkdir(self.testdir + '/state') + + pardir = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir)) + + print() + + subprocess.call([pardir + '/build/unitd', + # TODO '--no-daemon', + '--modules', pardir + '/build', + '--state', self.testdir + '/state', + '--pid', self.testdir + '/unit.pid', + '--log', self.testdir + '/unit.log', + '--control', 'unix:' + self.testdir + '/control.unit.sock']) + + time_wait = 0 + while time_wait < 5 and not (os.path.exists(self.testdir + '/unit.pid') + and os.path.exists(self.testdir + '/unit.log') + and os.path.exists(self.testdir + '/control.unit.sock')): + time.sleep(0.1) + time_wait += 0.1 + + # TODO dependency check + + def tearDown(self): + with open(self.testdir + '/unit.pid', 'r') as f: + pid = f.read().rstrip() + + subprocess.call(['kill', pid]) + + time_wait = 0 + while time_wait < 5 and os.path.exists(self.testdir + '/unit.pid'): + time.sleep(0.1) + time_wait += 0.1 + + if '--log' in sys.argv: + with open(self.testdir + '/unit.log', 'r') as f: + print(f.read()) + + if '--leave' not in sys.argv: + shutil.rmtree(self.testdir) + +class TestUnitControl(TestUnit): + + # TODO socket reuse + # TODO http client + + def get(self, path='/'): + + with self._control_sock() as sock: + sock.sendall(('GET ' + path + + ' HTTP/1.1\r\nHost: localhost\r\n\r\n').encode()) + r = self._recvall(sock) + + return self._body_json(r) + + def delete(self, path='/'): + + with self._control_sock() as sock: + sock.sendall(('DELETE ' + path + + ' HTTP/1.1\r\nHost: localhost\r\n\r\n').encode()) + r = self._recvall(sock) + + return self._body_json(r) + + def put(self, path='/', data=''): + + if isinstance(data, str): + data = data.encode() + + with self._control_sock() as sock: + sock.sendall(('PUT ' + path + (' HTTP/1.1\nHost: localhost\n' + 'Content-Length: ') + str(len(data)) + '\r\n\r\n').encode() + + data) + r = self._recvall(sock) + + return self._body_json(r) + + def _control_sock(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.testdir + '/control.unit.sock') + return sock + + def _recvall(self, sock, buff_size=4096): + data = '' + while True: + part = sock.recv(buff_size).decode() + data += part + if len(part) < buff_size: + break + + return data + + def _body_json(self, resp): + m = re.search('.*?\x0d\x0a?\x0d\x0a?(.*)', resp, re.M | re.S) + return json.loads(m.group(1))