diff options
33 files changed, 931 insertions, 918 deletions
diff --git a/test/test_access_log.py b/test/test_access_log.py index e0edd9fc..c58ee26f 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -2,13 +2,12 @@ import os import re import time from subprocess import call -import unittest -import unit +from unit.applications.lang.python import TestApplicationPython -class TestUnitAccessLog(unit.TestUnitApplicationPython): +class TestAccessLog(TestApplicationPython): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationPython().check_modules('python') def load(self, script): super().load(script) @@ -340,4 +339,4 @@ Connection: close if __name__ == '__main__': - TestUnitAccessLog.main() + TestAccessLog.main() diff --git a/test/test_configuration.py b/test/test_configuration.py index 4848f741..eb56a548 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,10 +1,10 @@ import unittest -import unit +from unit.control import TestControl -class TestUnitConfiguration(unit.TestUnitControl): +class TestConfiguration(TestControl): def setUpClass(): - unit.TestUnit().check_modules('python') + TestControl().check_modules('python') def test_json_empty(self): self.assertIn('error', self.conf(''), 'empty') @@ -358,4 +358,4 @@ class TestUnitConfiguration(unit.TestUnitControl): if __name__ == '__main__': - TestUnitConfiguration.main() + TestConfiguration.main() diff --git a/test/test_go_application.py b/test/test_go_application.py index 9b39b238..8c06d583 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -1,10 +1,10 @@ import unittest -import unit +from unit.applications.lang.go import TestApplicationGo -class TestUnitGoApplication(unit.TestUnitApplicationGo): +class TestGoApplication(TestApplicationGo): def setUpClass(): - unit.TestUnit().check_modules('go') + TestApplicationGo().check_modules('go') def test_go_application_variables(self): self.load('variables') @@ -184,4 +184,4 @@ class TestUnitGoApplication(unit.TestUnitApplicationGo): if __name__ == '__main__': - TestUnitGoApplication.main() + TestGoApplication.main() diff --git a/test/test_http_header.py b/test/test_http_header.py index 002a8826..d14514e2 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -1,10 +1,9 @@ -import unittest -import unit +from unit.applications.lang.python import TestApplicationPython -class TestUnitHTTPHeader(unit.TestUnitApplicationPython): +class TestHTTPHeader(TestApplicationPython): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationPython().check_modules('python') def test_http_header_value_leading_sp(self): self.load('custom_header') @@ -482,4 +481,4 @@ Connection: close if __name__ == '__main__': - TestUnitHTTPHeader.main() + TestHTTPHeader.main() diff --git a/test/test_java_application.py b/test/test_java_application.py index 30902c1b..71098711 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -1,11 +1,10 @@ import time -import unittest -import unit +from unit.applications.lang.java import TestApplicationJava -class TestUnitJavaApplication(unit.TestUnitApplicationJava): +class TestJavaApplication(TestApplicationJava): def setUpClass(): - unit.TestUnit().check_modules('java') + TestApplicationJava().check_modules('java') def test_java_application_cookies(self): self.load('cookies') @@ -1174,4 +1173,4 @@ class TestUnitJavaApplication(unit.TestUnitApplicationJava): if __name__ == '__main__': - TestUnitJavaApplication.main() + TestJavaApplication.main() diff --git a/test/test_node_application.py b/test/test_node_application.py index 1acf374f..242b0555 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -1,10 +1,10 @@ import unittest -import unit +from unit.applications.lang.node import TestApplicationNode -class TestUnitNodeApplication(unit.TestUnitApplicationNode): +class TestNodeApplication(TestApplicationNode): def setUpClass(): - u = unit.TestUnit().check_modules('node') + TestApplicationNode().check_modules('node') def test_node_application_basic(self): self.load('basic') @@ -387,4 +387,4 @@ class TestUnitNodeApplication(unit.TestUnitApplicationNode): if __name__ == '__main__': - TestUnitNodeApplication.main() + TestNodeApplication.main() diff --git a/test/test_perl_application.py b/test/test_perl_application.py index 050d9a17..521a227c 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -1,10 +1,10 @@ import unittest -import unit +from unit.applications.lang.perl import TestApplicationPerl -class TestUnitPerlApplication(unit.TestUnitApplicationPerl): +class TestPerlApplication(TestApplicationPerl): def setUpClass(): - unit.TestUnit().check_modules('perl') + TestApplicationPerl().check_modules('perl') def test_perl_application(self): self.load('variables') @@ -254,4 +254,4 @@ class TestUnitPerlApplication(unit.TestUnitApplicationPerl): if __name__ == '__main__': - TestUnitPerlApplication.main() + TestPerlApplication.main() diff --git a/test/test_php_application.py b/test/test_php_application.py index 5d2ce118..6c7f7f4b 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -1,11 +1,10 @@ -import unittest -import unit import re +import unittest +from unit.applications.lang.php import TestApplicationPHP - -class TestUnitPHPApplication(unit.TestUnitApplicationPHP): +class TestPHPApplication(TestApplicationPHP): def setUpClass(): - unit.TestUnit().check_modules('php') + TestApplicationPHP().check_modules('php') def before_disable_functions(self): body = self.get()['body'] @@ -422,4 +421,4 @@ class TestUnitPHPApplication(unit.TestUnitApplicationPHP): if __name__ == '__main__': - TestUnitPHPApplication.main() + TestPHPApplication.main() diff --git a/test/test_php_basic.py b/test/test_php_basic.py index 7ad2cae1..0e8caacf 100644 --- a/test/test_php_basic.py +++ b/test/test_php_basic.py @@ -1,10 +1,9 @@ -import unittest -import unit +from unit.control import TestControl -class TestUnitPHPBasic(unit.TestUnitControl): +class TestPHPBasic(TestControl): def setUpClass(): - unit.TestUnit().check_modules('php') + TestControl().check_modules('php') conf_app = { "app": { @@ -168,4 +167,4 @@ class TestUnitPHPBasic(unit.TestUnitControl): if __name__ == '__main__': - TestUnitPHPBasic.main() + TestPHPBasic.main() diff --git a/test/test_python_application.py b/test/test_python_application.py index 330bd60f..6c65e3b7 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -1,11 +1,11 @@ import time import unittest -import unit +from unit.applications.lang.python import TestApplicationPython -class TestUnitPythonApplication(unit.TestUnitApplicationPython): +class TestPythonApplication(TestApplicationPython): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationPython().check_modules('python') def test_python_application_variables(self): self.load('variables') @@ -461,4 +461,4 @@ Connection: close if __name__ == '__main__': - TestUnitPythonApplication.main() + TestPythonApplication.main() diff --git a/test/test_python_basic.py b/test/test_python_basic.py index f2a5b9f6..1869103f 100644 --- a/test/test_python_basic.py +++ b/test/test_python_basic.py @@ -1,10 +1,9 @@ -import unittest -import unit +from unit.control import TestControl -class TestUnitPythonBasic(unit.TestUnitControl): +class TestPythonBasic(TestControl): def setUpClass(): - unit.TestUnit().check_modules('python') + TestControl().check_modules('python') conf_app = { "app": { @@ -181,4 +180,4 @@ class TestUnitPythonBasic(unit.TestUnitControl): if __name__ == '__main__': - TestUnitPythonBasic.main() + TestPythonBasic.main() diff --git a/test/test_python_environment.py b/test/test_python_environment.py index 8ab82089..8debadda 100644 --- a/test/test_python_environment.py +++ b/test/test_python_environment.py @@ -1,10 +1,9 @@ -import unittest -import unit +from unit.applications.lang.python import TestApplicationPython -class TestUnitPythonEnvironment(unit.TestUnitApplicationPython): +class TestPythonEnvironment(TestApplicationPython): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationPython().check_modules('python') def test_python_environment_name_null(self): self.load('environment') @@ -178,4 +177,4 @@ class TestUnitPythonEnvironment(unit.TestUnitApplicationPython): if __name__ == '__main__': - TestUnitPythonEnvironment.main() + TestPythonEnvironment.main() diff --git a/test/test_python_procman.py b/test/test_python_procman.py index f4f53678..461846e3 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -2,12 +2,12 @@ import re import time import subprocess import unittest -import unit +from unit.applications.lang.python import TestApplicationPython -class TestUnitPythonProcman(unit.TestUnitApplicationPython): +class TestPythonProcman(TestApplicationPython): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationPython().check_modules('python') def pids_for_process(self): time.sleep(0.2) @@ -280,4 +280,4 @@ class TestUnitPythonProcman(unit.TestUnitApplicationPython): if __name__ == '__main__': - TestUnitPythonProcman.main() + TestPythonProcman.main() diff --git a/test/test_routing.py b/test/test_routing.py index b5c42576..bc84dd6f 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1,10 +1,9 @@ -import unittest -import unit +from unit.applications.proto import TestApplicationProto -class TestUnitRouting(unit.TestUnitApplicationProto): +class TestRouting(TestApplicationProto): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationProto().check_modules('python') def setUp(self): super().setUp() @@ -761,4 +760,4 @@ class TestUnitRouting(unit.TestUnitApplicationProto): if __name__ == '__main__': - TestUnitRouting.main() + TestRouting.main() diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index 0f2f6185..7e12ef83 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -1,10 +1,10 @@ import unittest -import unit +from unit.applications.lang.ruby import TestApplicationRuby -class TestUnitRubyApplication(unit.TestUnitApplicationRuby): +class TestRubyApplication(TestApplicationRuby): def setUpClass(): - unit.TestUnit().check_modules('ruby') + TestApplicationRuby().check_modules('ruby') def test_ruby_application(self): self.load('variables') @@ -348,4 +348,4 @@ class TestUnitRubyApplication(unit.TestUnitApplicationRuby): if __name__ == '__main__': - TestUnitRubyApplication.main() + TestRubyApplication.main() diff --git a/test/test_settings.py b/test/test_settings.py index 85bbfb44..b34883b8 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -1,12 +1,12 @@ import time import socket import unittest -import unit +from unit.applications.lang.python import TestApplicationPython -class TestUnitSettings(unit.TestUnitApplicationPython): +class TestSettings(TestApplicationPython): def setUpClass(): - unit.TestUnit().check_modules('python') + TestApplicationPython().check_modules('python') def test_settings_header_read_timeout(self): self.load('empty') @@ -226,4 +226,4 @@ Connection: close if __name__ == '__main__': - TestUnitSettings.main() + TestSettings.main() diff --git a/test/test_tls.py b/test/test_tls.py index 8b112f4e..954387d7 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -3,12 +3,13 @@ import ssl import time import subprocess import unittest -import unit +from unit.applications.tls import TestApplicationTLS +from unit.main import TestUnit -class TestUnitTLS(unit.TestUnitApplicationTLS): +class TestTLS(TestApplicationTLS): def setUpClass(): - unit.TestUnit().check_modules('python', 'openssl') + TestUnit().check_modules('python', 'openssl') def findall(self, pattern): with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: @@ -585,4 +586,4 @@ basicConstraints = critical,CA:TRUE""" ) if __name__ == '__main__': - TestUnitTLS.main() + TestTLS.main() diff --git a/test/unit.py b/test/unit.py deleted file mode 100644 index 4a613d1b..00000000 --- a/test/unit.py +++ /dev/null @@ -1,841 +0,0 @@ -import os -import re -import ssl -import sys -import json -import time -import shutil -import socket -import select -import argparse -import platform -import tempfile -import unittest -import subprocess -from multiprocessing import Process - - -class TestUnit(unittest.TestCase): - - pardir = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.pardir) - ) - architecture = platform.architecture()[0] - maxDiff = None - - detailed = False - save_log = False - - def __init__(self, methodName='runTest'): - super().__init__(methodName) - - if re.match(r'.*\/run\.py$', sys.argv[0]): - args, rest = TestUnit._parse_args() - - TestUnit._set_args(args) - - @classmethod - def main(cls): - args, rest = TestUnit._parse_args() - - for i, arg in enumerate(rest): - if arg[:5] == 'test_': - rest[i] = cls.__name__ + '.' + arg - - sys.argv = sys.argv[:1] + rest - - TestUnit._set_args(args) - - unittest.main() - - def setUp(self): - self._run() - - def tearDown(self): - self.stop() - - # detect errors and failures for current test - - def list2reason(exc_list): - if exc_list and exc_list[-1][0] is self: - return exc_list[-1][1] - - if hasattr(self, '_outcome'): - result = self.defaultTestResult() - self._feedErrorsToResult(result, self._outcome.errors) - else: - result = getattr( - self, '_outcomeForDoCleanups', self._resultForDoCleanups - ) - - success = not list2reason(result.errors) and not list2reason( - result.failures - ) - - # check unit.log for alerts - - unit_log = self.testdir + '/unit.log' - - with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: - self._check_alerts(f.read()) - - # remove unit.log - - if not TestUnit.save_log and success: - shutil.rmtree(self.testdir) - - else: - self._print_path_to_log() - - def check_modules(self, *modules): - self._run() - - for i in range(50): - with open(self.testdir + '/unit.log', 'r') as f: - log = f.read() - m = re.search('controller started', log) - - if m is None: - time.sleep(0.1) - else: - break - - if m is None: - self.stop() - exit("Unit is writing log too long") - - current_dir = os.path.dirname(os.path.abspath(__file__)) - - missed_module = '' - for module in modules: - if module == 'go': - env = os.environ.copy() - env['GOPATH'] = self.pardir + '/go' - - try: - process = subprocess.Popen( - [ - 'go', - 'build', - '-o', - self.testdir + '/go/check_module', - current_dir + '/go/empty/app.go', - ], - env=env, - ) - process.communicate() - - m = module if process.returncode == 0 else None - - except: - m = None - - elif module == 'node': - if os.path.isdir(self.pardir + '/node/node_modules'): - m = module - else: - m = None - - elif module == 'openssl': - try: - subprocess.check_output(['which', 'openssl']) - - output = subprocess.check_output( - [self.pardir + '/build/unitd', '--version'], - stderr=subprocess.STDOUT, - ) - - m = re.search('--openssl', output.decode()) - - except: - m = None - - else: - m = re.search('module: ' + module, log) - - if m is None: - missed_module = module - break - - self.stop() - self._check_alerts(log) - shutil.rmtree(self.testdir) - - if missed_module: - raise unittest.SkipTest('Unit has no ' + missed_module + ' module') - - def stop(self): - if self._started: - self._stop() - - def _run(self): - self.testdir = tempfile.mkdtemp(prefix='unit-test-') - - os.mkdir(self.testdir + '/state') - - print() - - def _run_unit(): - subprocess.call( - [ - self.pardir + '/build/unitd', - '--no-daemon', - '--modules', self.pardir + '/build', - '--state', self.testdir + '/state', - '--pid', self.testdir + '/unit.pid', - '--log', self.testdir + '/unit.log', - '--control', 'unix:' + self.testdir + '/control.unit.sock', - ] - ) - - self._p = Process(target=_run_unit) - self._p.start() - - if not self.waitforfiles( - self.testdir + '/unit.pid', - self.testdir + '/unit.log', - self.testdir + '/control.unit.sock', - ): - exit("Could not start unit") - - self._started = True - - self.skip_alerts = [ - r'read signalfd\(4\) failed', - r'sendmsg.+failed', - r'recvmsg.+failed', - ] - self.skip_sanitizer = False - - def _stop(self): - with open(self.testdir + '/unit.pid', 'r') as f: - pid = f.read().rstrip() - - subprocess.call(['kill', '-s', 'QUIT', pid]) - - for i in range(50): - if not os.path.exists(self.testdir + '/unit.pid'): - break - time.sleep(0.1) - - if os.path.exists(self.testdir + '/unit.pid'): - exit("Could not terminate unit") - - self._started = False - - self._p.join(timeout=1) - self._terminate_process(self._p) - - def _terminate_process(self, process): - if process.is_alive(): - process.terminate() - process.join(timeout=5) - - if process.is_alive(): - exit("Could not terminate process " + process.pid) - - if process.exitcode: - exit("Child process terminated with code " + str(process.exitcode)) - - def _check_alerts(self, log): - found = False - - alerts = re.findall('.+\[alert\].+', log) - - if alerts: - print('All alerts/sanitizer errors found in log:') - [print(alert) for alert in alerts] - found = True - - if self.skip_alerts: - for skip in self.skip_alerts: - alerts = [al for al in alerts if re.search(skip, al) is None] - - if alerts: - self._print_path_to_log() - self.assertFalse(alerts, 'alert(s)') - - if not self.skip_sanitizer: - sanitizer_errors = re.findall('.+Sanitizer.+', log) - - if sanitizer_errors: - self._print_path_to_log() - self.assertFalse(sanitizer_errors, 'sanitizer error(s)') - - if found: - print('skipped.') - - def waitforfiles(self, *files): - for i in range(50): - wait = False - ret = False - - for f in files: - if not os.path.exists(f): - wait = True - break - - if wait: - time.sleep(0.1) - - else: - ret = True - break - - return ret - - @staticmethod - def _parse_args(): - parser = argparse.ArgumentParser(add_help=False) - - parser.add_argument( - '-d', - '--detailed', - dest='detailed', - action='store_true', - help='Detailed output for tests', - ) - parser.add_argument( - '-l', - '--log', - dest='save_log', - action='store_true', - help='Save unit.log after the test execution', - ) - - return parser.parse_known_args() - - @staticmethod - def _set_args(args): - TestUnit.detailed = args.detailed - TestUnit.save_log = args.save_log - - def _print_path_to_log(self): - print('Path to unit.log:\n' + self.testdir + '/unit.log') - - -class TestUnitHTTP(TestUnit): - def http(self, start_str, **kwargs): - sock_type = ( - 'ipv4' if 'sock_type' not in kwargs else kwargs['sock_type'] - ) - port = 7080 if 'port' not in kwargs else kwargs['port'] - url = '/' if 'url' not in kwargs else kwargs['url'] - http = 'HTTP/1.0' if 'http_10' in kwargs else 'HTTP/1.1' - - headers = ( - {'Host': 'localhost', 'Connection': 'close'} - if 'headers' not in kwargs - else kwargs['headers'] - ) - - body = b'' if 'body' not in kwargs else kwargs['body'] - crlf = '\r\n' - - if 'addr' not in kwargs: - addr = '::1' if sock_type == 'ipv6' else '127.0.0.1' - else: - addr = kwargs['addr'] - - sock_types = { - 'ipv4': socket.AF_INET, - 'ipv6': socket.AF_INET6, - 'unix': socket.AF_UNIX, - } - - if 'sock' not in kwargs: - sock = socket.socket(sock_types[sock_type], socket.SOCK_STREAM) - - if ( - sock_type == sock_types['ipv4'] - or sock_type == sock_types['ipv6'] - ): - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - - if 'wrapper' in kwargs: - sock = kwargs['wrapper'](sock) - - connect_args = addr if sock_type == 'unix' else (addr, port) - try: - sock.connect(connect_args) - except ConnectionRefusedError: - sock.close() - return None - - else: - sock = kwargs['sock'] - - if 'raw' not in kwargs: - req = ' '.join([start_str, url, http]) + crlf - - if body is not b'': - if isinstance(body, str): - body = body.encode() - - if 'Content-Length' not in headers: - headers['Content-Length'] = len(body) - - for header, value in headers.items(): - if isinstance(value, list): - for v in value: - req += header + ': ' + str(v) + crlf - - else: - req += header + ': ' + str(value) + crlf - - req = (req + crlf).encode() + body - - else: - req = start_str - - sock.sendall(req) - - if TestUnit.detailed: - print('>>>', req, sep='\n') - - resp = '' - - if 'no_recv' not in kwargs: - enc = 'utf-8' if 'encoding' not in kwargs else kwargs['encoding'] - read_timeout = ( - 5 if 'read_timeout' not in kwargs else kwargs['read_timeout'] - ) - resp = self.recvall(sock, read_timeout=read_timeout).decode(enc) - - if TestUnit.detailed: - print('<<<', resp.encode('utf-8'), sep='\n') - - if 'raw_resp' not in kwargs: - resp = self._resp_to_dict(resp) - - if 'start' not in kwargs: - sock.close() - return resp - - return (resp, sock) - - def delete(self, **kwargs): - return self.http('DELETE', **kwargs) - - def get(self, **kwargs): - return self.http('GET', **kwargs) - - def post(self, **kwargs): - return self.http('POST', **kwargs) - - def put(self, **kwargs): - return self.http('PUT', **kwargs) - - def recvall(self, sock, read_timeout=5, buff_size=4096): - data = b'' - while select.select([sock], [], [], read_timeout)[0]: - try: - part = sock.recv(buff_size) - except: - break - - data += part - - if not len(part): - break - - return data - - def _resp_to_dict(self, resp): - m = re.search('(.*?\x0d\x0a?)\x0d\x0a?(.*)', resp, re.M | re.S) - - if not m: - return {} - - headers_text, body = m.group(1), m.group(2) - - p = re.compile('(.*?)\x0d\x0a?', re.M | re.S) - headers_lines = p.findall(headers_text) - - status = re.search( - '^HTTP\/\d\.\d\s(\d+)|$', headers_lines.pop(0) - ).group(1) - - headers = {} - for line in headers_lines: - m = re.search('(.*)\:\s(.*)', line) - - if m.group(1) not in headers: - headers[m.group(1)] = m.group(2) - - elif isinstance(headers[m.group(1)], list): - headers[m.group(1)].append(m.group(2)) - - else: - headers[m.group(1)] = [headers[m.group(1)], m.group(2)] - - return {'status': int(status), 'headers': headers, 'body': body} - - -class TestUnitControl(TestUnitHTTP): - - # TODO socket reuse - # TODO http client - - def conf(self, conf, path='/config'): - if isinstance(conf, dict) or isinstance(conf, list): - conf = json.dumps(conf) - - if path[:1] != '/': - path = '/config/' + path - - return json.loads( - self.put( - url=path, - body=conf, - sock_type='unix', - addr=self.testdir + '/control.unit.sock', - )['body'] - ) - - def conf_get(self, path='/config'): - if path[:1] != '/': - path = '/config/' + path - - return json.loads( - self.get( - url=path, - sock_type='unix', - addr=self.testdir + '/control.unit.sock', - )['body'] - ) - - def conf_delete(self, path='/config'): - if path[:1] != '/': - path = '/config/' + path - - return json.loads( - self.delete( - url=path, - sock_type='unix', - addr=self.testdir + '/control.unit.sock', - )['body'] - ) - - -class TestUnitApplicationProto(TestUnitControl): - - current_dir = os.path.dirname(os.path.abspath(__file__)) - - def sec_epoch(self): - return time.mktime(time.gmtime()) - - def date_to_sec_epoch(self, date, template='%a, %d %b %Y %H:%M:%S %Z'): - return time.mktime(time.strptime(date, template)) - - def search_in_log(self, pattern): - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: - return re.search(pattern, f.read()) - - -class TestUnitApplicationPython(TestUnitApplicationProto): - def load(self, script, name=None): - if name is None: - name = script - - script_path = self.current_dir + '/python/' + script - - self.conf( - { - "listeners": {"*:7080": {"application": name}}, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": script_path, - "working_directory": script_path, - "module": "wsgi", - } - }, - } - ) - - -class TestUnitApplicationRuby(TestUnitApplicationProto): - def load(self, script, name='config.ru'): - script_path = self.current_dir + '/ruby/' + script - - self.conf( - { - "listeners": {"*:7080": {"application": script}}, - "applications": { - script: { - "type": "ruby", - "processes": {"spare": 0}, - "working_directory": script_path, - "script": script_path + '/' + name, - } - }, - } - ) - - -class TestUnitApplicationPHP(TestUnitApplicationProto): - def load(self, script, name='index.php'): - script_path = self.current_dir + '/php/' + script - - self.conf( - { - "listeners": {"*:7080": {"application": script}}, - "applications": { - script: { - "type": "php", - "processes": {"spare": 0}, - "root": script_path, - "working_directory": script_path, - "index": name, - } - }, - } - ) - - -class TestUnitApplicationGo(TestUnitApplicationProto): - def load(self, script, name='app'): - - if not os.path.isdir(self.testdir + '/go'): - os.mkdir(self.testdir + '/go') - - go_app_path = self.current_dir + '/go/' - - env = os.environ.copy() - env['GOPATH'] = self.pardir + '/go' - process = subprocess.Popen( - [ - 'go', - 'build', - '-o', - self.testdir + '/go/' + name, - go_app_path + script + '/' + name + '.go', - ], - env=env, - ) - process.communicate() - - self.conf( - { - "listeners": {"*:7080": {"application": script}}, - "applications": { - script: { - "type": "external", - "processes": {"spare": 0}, - "working_directory": go_app_path + script, - "executable": self.testdir + '/go/' + name, - } - }, - } - ) - - -class TestUnitApplicationNode(TestUnitApplicationProto): - def load(self, script, name='app.js'): - - # copy application - - shutil.copytree( - self.current_dir + '/node/' + script, self.testdir + '/node' - ) - - # link modules - - os.symlink( - self.pardir + '/node/node_modules', - self.testdir + '/node/node_modules', - ) - - self.conf( - { - "listeners": {"*:7080": {"application": script}}, - "applications": { - script: { - "type": "external", - "processes": {"spare": 0}, - "working_directory": self.testdir + '/node', - "executable": name, - } - }, - } - ) - - -class TestUnitApplicationJava(TestUnitApplicationProto): - def load(self, script, name='app'): - - app_path = self.testdir + '/java' - web_inf_path = app_path + '/WEB-INF/' - classes_path = web_inf_path + 'classes/' - - script_path = self.current_dir + '/java/' + script + '/' - - if not os.path.isdir(app_path): - os.makedirs(app_path) - - src = [] - - for f in os.listdir(script_path): - if f.endswith('.java'): - src.append(script_path + f) - continue - - if f.startswith('.') or f == 'Makefile': - continue - - if os.path.isdir(script_path + f): - if f == 'WEB-INF': - continue - - shutil.copytree(script_path + f, app_path + '/' + f) - continue - - if f == 'web.xml': - if not os.path.isdir(web_inf_path): - os.makedirs(web_inf_path) - - shutil.copy2(script_path + f, web_inf_path) - else: - shutil.copy2(script_path + f, app_path) - - if src: - if not os.path.isdir(classes_path): - os.makedirs(classes_path) - - tomcat_jar = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar' - - javac = [ - 'javac', - '-encoding', 'utf-8', - '-d', classes_path, - '-classpath', tomcat_jar, - ] - javac.extend(src) - - process = subprocess.Popen(javac) - process.communicate() - - self.conf( - { - "listeners": {"*:7080": {"application": script}}, - "applications": { - script: { - "unit_jars": self.pardir + '/build', - "type": "java", - "processes": {"spare": 0}, - "working_directory": script_path, - "webapp": app_path, - } - }, - } - ) - - -class TestUnitApplicationPerl(TestUnitApplicationProto): - def load(self, script, name='psgi.pl'): - script_path = self.current_dir + '/perl/' + script - - self.conf( - { - "listeners": {"*:7080": {"application": script}}, - "applications": { - script: { - "type": "perl", - "processes": {"spare": 0}, - "working_directory": script_path, - "script": script_path + '/' + name, - } - }, - } - ) - - -class TestUnitApplicationTLS(TestUnitApplicationProto): - def __init__(self, test): - super().__init__(test) - - self.context = ssl.create_default_context() - self.context.check_hostname = False - self.context.verify_mode = ssl.CERT_NONE - - def certificate(self, name='default', load=True): - subprocess.call( - [ - 'openssl', - 'req', - '-x509', - '-new', - '-subj', '/CN=' + name + '/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/' + name + '.crt', - '-keyout', self.testdir + '/' + name + '.key', - ] - ) - - if load: - self.certificate_load(name) - - def certificate_load(self, crt, key=None): - if key is None: - key = crt - - key_path = self.testdir + '/' + key + '.key' - crt_path = self.testdir + '/' + crt + '.crt' - - with open(key_path, 'rb') as k, open(crt_path, 'rb') as c: - return self.conf(k.read() + c.read(), '/certificates/' + crt) - - def get_ssl(self, **kwargs): - return self.get(wrapper=self.context.wrap_socket, **kwargs) - - def post_ssl(self, **kwargs): - return self.post(wrapper=self.context.wrap_socket, **kwargs) - - def get_server_certificate(self, addr=('127.0.0.1', 7080)): - - ssl_list = dir(ssl) - - if 'PROTOCOL_TLS' in ssl_list: - ssl_version = ssl.PROTOCOL_TLS - - elif 'PROTOCOL_TLSv1_2' in ssl_list: - ssl_version = ssl.PROTOCOL_TLSv1_2 - - else: - ssl_version = ssl.PROTOCOL_TLSv1_1 - - return ssl.get_server_certificate(addr, ssl_version=ssl_version) - - def load(self, script, name=None): - if name is None: - name = script - - # create default openssl configuration - - with open(self.testdir + '/openssl.conf', 'w') as f: - f.write( - """[ req ] -default_bits = 1024 -encrypt_key = no -distinguished_name = req_distinguished_name -[ req_distinguished_name ]""" - ) - - script_path = self.current_dir + '/python/' + script - - self.conf( - { - "listeners": {"*:7080": {"application": name}}, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": script_path, - "working_directory": script_path, - "module": "wsgi", - } - }, - } - ) diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/unit/__init__.py diff --git a/test/unit/applications/__init__.py b/test/unit/applications/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/unit/applications/__init__.py diff --git a/test/unit/applications/lang/__init__.py b/test/unit/applications/lang/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/unit/applications/lang/__init__.py diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py new file mode 100644 index 00000000..4852459c --- /dev/null +++ b/test/unit/applications/lang/go.py @@ -0,0 +1,40 @@ +import os +from subprocess import Popen +from unit.applications.proto import TestApplicationProto + + +class TestApplicationGo(TestApplicationProto): + def load(self, script, name='app'): + + if not os.path.isdir(self.testdir + '/go'): + os.mkdir(self.testdir + '/go') + + go_app_path = self.current_dir + '/go/' + + env = os.environ.copy() + env['GOPATH'] = self.pardir + '/go' + process = Popen( + [ + 'go', + 'build', + '-o', + self.testdir + '/go/' + name, + go_app_path + script + '/' + name + '.go', + ], + env=env, + ) + process.communicate() + + self.conf( + { + "listeners": {"*:7080": {"application": script}}, + "applications": { + script: { + "type": "external", + "processes": {"spare": 0}, + "working_directory": go_app_path + script, + "executable": self.testdir + '/go/' + name, + } + }, + } + ) diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py new file mode 100644 index 00000000..91bfb9ec --- /dev/null +++ b/test/unit/applications/lang/java.py @@ -0,0 +1,74 @@ +import os +import shutil +from subprocess import Popen +from unit.applications.proto import TestApplicationProto + + +class TestApplicationJava(TestApplicationProto): + def load(self, script, name='app'): + + app_path = self.testdir + '/java' + web_inf_path = app_path + '/WEB-INF/' + classes_path = web_inf_path + 'classes/' + + script_path = self.current_dir + '/java/' + script + '/' + + if not os.path.isdir(app_path): + os.makedirs(app_path) + + src = [] + + for f in os.listdir(script_path): + if f.endswith('.java'): + src.append(script_path + f) + continue + + if f.startswith('.') or f == 'Makefile': + continue + + if os.path.isdir(script_path + f): + if f == 'WEB-INF': + continue + + shutil.copytree(script_path + f, app_path + '/' + f) + continue + + if f == 'web.xml': + if not os.path.isdir(web_inf_path): + os.makedirs(web_inf_path) + + shutil.copy2(script_path + f, web_inf_path) + else: + shutil.copy2(script_path + f, app_path) + + if src: + if not os.path.isdir(classes_path): + os.makedirs(classes_path) + + tomcat_jar = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar' + + javac = [ + 'javac', + '-encoding', 'utf-8', + '-d', classes_path, + '-classpath', tomcat_jar, + ] + javac.extend(src) + + process = Popen(javac) + process.communicate() + + self.conf( + { + "listeners": {"*:7080": {"application": script}}, + "applications": { + script: { + "unit_jars": self.pardir + '/build', + "type": "java", + "processes": {"spare": 0}, + "working_directory": script_path, + "webapp": app_path, + } + }, + } + ) diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py new file mode 100644 index 00000000..f1b99cc7 --- /dev/null +++ b/test/unit/applications/lang/node.py @@ -0,0 +1,34 @@ +import os +import shutil +from unit.applications.proto import TestApplicationProto + + +class TestApplicationNode(TestApplicationProto): + def load(self, script, name='app.js'): + + # copy application + + shutil.copytree( + self.current_dir + '/node/' + script, self.testdir + '/node' + ) + + # link modules + + os.symlink( + self.pardir + '/node/node_modules', + self.testdir + '/node/node_modules', + ) + + self.conf( + { + "listeners": {"*:7080": {"application": script}}, + "applications": { + script: { + "type": "external", + "processes": {"spare": 0}, + "working_directory": self.testdir + '/node', + "executable": name, + } + }, + } + ) diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py new file mode 100644 index 00000000..6970873d --- /dev/null +++ b/test/unit/applications/lang/perl.py @@ -0,0 +1,20 @@ +from unit.applications.proto import TestApplicationProto + + +class TestApplicationPerl(TestApplicationProto): + def load(self, script, name='psgi.pl'): + script_path = self.current_dir + '/perl/' + script + + self.conf( + { + "listeners": {"*:7080": {"application": script}}, + "applications": { + script: { + "type": "perl", + "processes": {"spare": 0}, + "working_directory": script_path, + "script": script_path + '/' + name, + } + }, + } + ) diff --git a/test/unit/applications/lang/php.py b/test/unit/applications/lang/php.py new file mode 100644 index 00000000..c4043764 --- /dev/null +++ b/test/unit/applications/lang/php.py @@ -0,0 +1,21 @@ +from unit.applications.proto import TestApplicationProto + + +class TestApplicationPHP(TestApplicationProto): + def load(self, script, name='index.php'): + script_path = self.current_dir + '/php/' + script + + self.conf( + { + "listeners": {"*:7080": {"application": script}}, + "applications": { + script: { + "type": "php", + "processes": {"spare": 0}, + "root": script_path, + "working_directory": script_path, + "index": name, + } + }, + } + ) diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py new file mode 100644 index 00000000..8c2c8707 --- /dev/null +++ b/test/unit/applications/lang/python.py @@ -0,0 +1,24 @@ +from unit.applications.proto import TestApplicationProto + + +class TestApplicationPython(TestApplicationProto): + def load(self, script, name=None): + if name is None: + name = script + + script_path = self.current_dir + '/python/' + script + + self.conf( + { + "listeners": {"*:7080": {"application": name}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": script_path, + "working_directory": script_path, + "module": "wsgi", + } + }, + } + ) diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py new file mode 100644 index 00000000..94086d26 --- /dev/null +++ b/test/unit/applications/lang/ruby.py @@ -0,0 +1,20 @@ +from unit.applications.proto import TestApplicationProto + + +class TestApplicationRuby(TestApplicationProto): + def load(self, script, name='config.ru'): + script_path = self.current_dir + '/ruby/' + script + + self.conf( + { + "listeners": {"*:7080": {"application": script}}, + "applications": { + script: { + "type": "ruby", + "processes": {"spare": 0}, + "working_directory": script_path, + "script": script_path + '/' + name, + } + }, + } + ) diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py new file mode 100644 index 00000000..17dfed35 --- /dev/null +++ b/test/unit/applications/proto.py @@ -0,0 +1,15 @@ +import re +import time +from unit.control import TestControl + + +class TestApplicationProto(TestControl): + def sec_epoch(self): + return time.mktime(time.gmtime()) + + def date_to_sec_epoch(self, date, template='%a, %d %b %Y %H:%M:%S %Z'): + return time.mktime(time.strptime(date, template)) + + def search_in_log(self, pattern): + with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + return re.search(pattern, f.read()) diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py new file mode 100644 index 00000000..1e1f3675 --- /dev/null +++ b/test/unit/applications/tls.py @@ -0,0 +1,92 @@ +import ssl +import subprocess +from unit.applications.proto import TestApplicationProto + + +class TestApplicationTLS(TestApplicationProto): + def __init__(self, test): + super().__init__(test) + + self.context = ssl.create_default_context() + self.context.check_hostname = False + self.context.verify_mode = ssl.CERT_NONE + + def certificate(self, name='default', load=True): + subprocess.call( + [ + 'openssl', + 'req', + '-x509', + '-new', + '-subj', '/CN=' + name + '/', + '-config', self.testdir + '/openssl.conf', + '-out', self.testdir + '/' + name + '.crt', + '-keyout', self.testdir + '/' + name + '.key', + ] + ) + + if load: + self.certificate_load(name) + + def certificate_load(self, crt, key=None): + if key is None: + key = crt + + key_path = self.testdir + '/' + key + '.key' + crt_path = self.testdir + '/' + crt + '.crt' + + with open(key_path, 'rb') as k, open(crt_path, 'rb') as c: + return self.conf(k.read() + c.read(), '/certificates/' + crt) + + def get_ssl(self, **kwargs): + return self.get(wrapper=self.context.wrap_socket, **kwargs) + + def post_ssl(self, **kwargs): + return self.post(wrapper=self.context.wrap_socket, **kwargs) + + def get_server_certificate(self, addr=('127.0.0.1', 7080)): + + ssl_list = dir(ssl) + + if 'PROTOCOL_TLS' in ssl_list: + ssl_version = ssl.PROTOCOL_TLS + + elif 'PROTOCOL_TLSv1_2' in ssl_list: + ssl_version = ssl.PROTOCOL_TLSv1_2 + + else: + ssl_version = ssl.PROTOCOL_TLSv1_1 + + return ssl.get_server_certificate(addr, ssl_version=ssl_version) + + def load(self, script, name=None): + if name is None: + name = script + + # create default openssl configuration + + with open(self.testdir + '/openssl.conf', 'w') as f: + f.write( + """[ req ] +default_bits = 1024 +encrypt_key = no +distinguished_name = req_distinguished_name +[ req_distinguished_name ]""" + ) + + script_path = self.current_dir + '/python/' + script + + self.conf( + { + "listeners": {"*:7080": {"application": name}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": script_path, + "working_directory": script_path, + "module": "wsgi", + } + }, + } + ) diff --git a/test/unit/control.py b/test/unit/control.py new file mode 100644 index 00000000..c4cfc4ce --- /dev/null +++ b/test/unit/control.py @@ -0,0 +1,48 @@ +import json +from unit.http import TestHTTP + + +class TestControl(TestHTTP): + + # TODO socket reuse + # TODO http client + + def conf(self, conf, path='/config'): + if isinstance(conf, dict) or isinstance(conf, list): + conf = json.dumps(conf) + + if path[:1] != '/': + path = '/config/' + path + + return json.loads( + self.put( + url=path, + body=conf, + sock_type='unix', + addr=self.testdir + '/control.unit.sock', + )['body'] + ) + + def conf_get(self, path='/config'): + if path[:1] != '/': + path = '/config/' + path + + return json.loads( + self.get( + url=path, + sock_type='unix', + addr=self.testdir + '/control.unit.sock', + )['body'] + ) + + def conf_delete(self, path='/config'): + if path[:1] != '/': + path = '/config/' + path + + return json.loads( + self.delete( + url=path, + sock_type='unix', + addr=self.testdir + '/control.unit.sock', + )['body'] + ) diff --git a/test/unit/http.py b/test/unit/http.py new file mode 100644 index 00000000..cbe6e612 --- /dev/null +++ b/test/unit/http.py @@ -0,0 +1,162 @@ +import re +import socket +import select +from unit.main import TestUnit + + +class TestHTTP(TestUnit): + def http(self, start_str, **kwargs): + sock_type = ( + 'ipv4' if 'sock_type' not in kwargs else kwargs['sock_type'] + ) + port = 7080 if 'port' not in kwargs else kwargs['port'] + url = '/' if 'url' not in kwargs else kwargs['url'] + http = 'HTTP/1.0' if 'http_10' in kwargs else 'HTTP/1.1' + + headers = ( + {'Host': 'localhost', 'Connection': 'close'} + if 'headers' not in kwargs + else kwargs['headers'] + ) + + body = b'' if 'body' not in kwargs else kwargs['body'] + crlf = '\r\n' + + if 'addr' not in kwargs: + addr = '::1' if sock_type == 'ipv6' else '127.0.0.1' + else: + addr = kwargs['addr'] + + sock_types = { + 'ipv4': socket.AF_INET, + 'ipv6': socket.AF_INET6, + 'unix': socket.AF_UNIX, + } + + if 'sock' not in kwargs: + sock = socket.socket(sock_types[sock_type], socket.SOCK_STREAM) + + if ( + sock_type == sock_types['ipv4'] + or sock_type == sock_types['ipv6'] + ): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if 'wrapper' in kwargs: + sock = kwargs['wrapper'](sock) + + connect_args = addr if sock_type == 'unix' else (addr, port) + try: + sock.connect(connect_args) + except ConnectionRefusedError: + sock.close() + return None + + else: + sock = kwargs['sock'] + + if 'raw' not in kwargs: + req = ' '.join([start_str, url, http]) + crlf + + if body is not b'': + if isinstance(body, str): + body = body.encode() + + if 'Content-Length' not in headers: + headers['Content-Length'] = len(body) + + for header, value in headers.items(): + if isinstance(value, list): + for v in value: + req += header + ': ' + str(v) + crlf + + else: + req += header + ': ' + str(value) + crlf + + req = (req + crlf).encode() + body + + else: + req = start_str + + sock.sendall(req) + + if TestUnit.detailed: + print('>>>', req, sep='\n') + + resp = '' + + if 'no_recv' not in kwargs: + enc = 'utf-8' if 'encoding' not in kwargs else kwargs['encoding'] + read_timeout = ( + 5 if 'read_timeout' not in kwargs else kwargs['read_timeout'] + ) + resp = self.recvall(sock, read_timeout=read_timeout).decode(enc) + + if TestUnit.detailed: + print('<<<', resp.encode('utf-8'), sep='\n') + + if 'raw_resp' not in kwargs: + resp = self._resp_to_dict(resp) + + if 'start' not in kwargs: + sock.close() + return resp + + return (resp, sock) + + def delete(self, **kwargs): + return self.http('DELETE', **kwargs) + + def get(self, **kwargs): + return self.http('GET', **kwargs) + + def post(self, **kwargs): + return self.http('POST', **kwargs) + + def put(self, **kwargs): + return self.http('PUT', **kwargs) + + def recvall(self, sock, read_timeout=5, buff_size=4096): + data = b'' + while select.select([sock], [], [], read_timeout)[0]: + try: + part = sock.recv(buff_size) + except: + break + + data += part + + if not len(part): + break + + return data + + def _resp_to_dict(self, resp): + m = re.search('(.*?\x0d\x0a?)\x0d\x0a?(.*)', resp, re.M | re.S) + + if not m: + return {} + + headers_text, body = m.group(1), m.group(2) + + p = re.compile('(.*?)\x0d\x0a?', re.M | re.S) + headers_lines = p.findall(headers_text) + + status = re.search( + '^HTTP\/\d\.\d\s(\d+)|$', headers_lines.pop(0) + ).group(1) + + headers = {} + for line in headers_lines: + m = re.search('(.*)\:\s(.*)', line) + + if m.group(1) not in headers: + headers[m.group(1)] = m.group(2) + + elif isinstance(headers[m.group(1)], list): + headers[m.group(1)].append(m.group(2)) + + else: + headers[m.group(1)] = [headers[m.group(1)], m.group(2)] + + return {'status': int(status), 'headers': headers, 'body': body} diff --git a/test/unit/main.py b/test/unit/main.py new file mode 100644 index 00000000..247f3fbf --- /dev/null +++ b/test/unit/main.py @@ -0,0 +1,311 @@ +import os +import re +import sys +import time +import shutil +import argparse +import platform +import tempfile +import unittest +import subprocess +from multiprocessing import Process + + +class TestUnit(unittest.TestCase): + + current_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir) + ) + pardir = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) + ) + architecture = platform.architecture()[0] + maxDiff = None + + detailed = False + save_log = False + + def __init__(self, methodName='runTest'): + super().__init__(methodName) + + if re.match(r'.*\/run\.py$', sys.argv[0]): + args, rest = TestUnit._parse_args() + + TestUnit._set_args(args) + + @classmethod + def main(cls): + args, rest = TestUnit._parse_args() + + for i, arg in enumerate(rest): + if arg[:5] == 'test_': + rest[i] = cls.__name__ + '.' + arg + + sys.argv = sys.argv[:1] + rest + + TestUnit._set_args(args) + + unittest.main() + + def setUp(self): + self._run() + + def tearDown(self): + self.stop() + + # detect errors and failures for current test + + def list2reason(exc_list): + if exc_list and exc_list[-1][0] is self: + return exc_list[-1][1] + + if hasattr(self, '_outcome'): + result = self.defaultTestResult() + self._feedErrorsToResult(result, self._outcome.errors) + else: + result = getattr( + self, '_outcomeForDoCleanups', self._resultForDoCleanups + ) + + success = not list2reason(result.errors) and not list2reason( + result.failures + ) + + # check unit.log for alerts + + unit_log = self.testdir + '/unit.log' + + with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: + self._check_alerts(f.read()) + + # remove unit.log + + if not TestUnit.save_log and success: + shutil.rmtree(self.testdir) + + else: + self._print_path_to_log() + + def check_modules(self, *modules): + self._run() + + for i in range(50): + with open(self.testdir + '/unit.log', 'r') as f: + log = f.read() + m = re.search('controller started', log) + + if m is None: + time.sleep(0.1) + else: + break + + if m is None: + self.stop() + exit("Unit is writing log too long") + + missed_module = '' + for module in modules: + if module == 'go': + env = os.environ.copy() + env['GOPATH'] = self.pardir + '/go' + + try: + process = subprocess.Popen( + [ + 'go', + 'build', + '-o', + self.testdir + '/go/check_module', + self.current_dir + '/go/empty/app.go', + ], + env=env, + ) + process.communicate() + + m = module if process.returncode == 0 else None + + except: + m = None + + elif module == 'node': + if os.path.isdir(self.pardir + '/node/node_modules'): + m = module + else: + m = None + + elif module == 'openssl': + try: + subprocess.check_output(['which', 'openssl']) + + output = subprocess.check_output( + [self.pardir + '/build/unitd', '--version'], + stderr=subprocess.STDOUT, + ) + + m = re.search('--openssl', output.decode()) + + except: + m = None + + else: + m = re.search('module: ' + module, log) + + if m is None: + missed_module = module + break + + self.stop() + self._check_alerts(log) + shutil.rmtree(self.testdir) + + if missed_module: + raise unittest.SkipTest('Unit has no ' + missed_module + ' module') + + def stop(self): + if self._started: + self._stop() + + def _run(self): + self.testdir = tempfile.mkdtemp(prefix='unit-test-') + + os.mkdir(self.testdir + '/state') + + print() + + def _run_unit(): + subprocess.call( + [ + self.pardir + '/build/unitd', + '--no-daemon', + '--modules', self.pardir + '/build', + '--state', self.testdir + '/state', + '--pid', self.testdir + '/unit.pid', + '--log', self.testdir + '/unit.log', + '--control', 'unix:' + self.testdir + '/control.unit.sock', + ] + ) + + self._p = Process(target=_run_unit) + self._p.start() + + if not self.waitforfiles( + self.testdir + '/unit.pid', + self.testdir + '/unit.log', + self.testdir + '/control.unit.sock', + ): + exit("Could not start unit") + + self._started = True + + self.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + self.skip_sanitizer = False + + def _stop(self): + with open(self.testdir + '/unit.pid', 'r') as f: + pid = f.read().rstrip() + + subprocess.call(['kill', '-s', 'QUIT', pid]) + + for i in range(50): + if not os.path.exists(self.testdir + '/unit.pid'): + break + time.sleep(0.1) + + if os.path.exists(self.testdir + '/unit.pid'): + exit("Could not terminate unit") + + self._started = False + + self._p.join(timeout=1) + self._terminate_process(self._p) + + def _terminate_process(self, process): + if process.is_alive(): + process.terminate() + process.join(timeout=5) + + if process.is_alive(): + exit("Could not terminate process " + process.pid) + + if process.exitcode: + exit("Child process terminated with code " + str(process.exitcode)) + + def _check_alerts(self, log): + found = False + + alerts = re.findall('.+\[alert\].+', log) + + if alerts: + print('All alerts/sanitizer errors found in log:') + [print(alert) for alert in alerts] + found = True + + if self.skip_alerts: + for skip in self.skip_alerts: + alerts = [al for al in alerts if re.search(skip, al) is None] + + if alerts: + self._print_path_to_log() + self.assertFalse(alerts, 'alert(s)') + + if not self.skip_sanitizer: + sanitizer_errors = re.findall('.+Sanitizer.+', log) + + if sanitizer_errors: + self._print_path_to_log() + self.assertFalse(sanitizer_errors, 'sanitizer error(s)') + + if found: + print('skipped.') + + def waitforfiles(self, *files): + for i in range(50): + wait = False + ret = False + + for f in files: + if not os.path.exists(f): + wait = True + break + + if wait: + time.sleep(0.1) + + else: + ret = True + break + + return ret + + @staticmethod + def _parse_args(): + parser = argparse.ArgumentParser(add_help=False) + + parser.add_argument( + '-d', + '--detailed', + dest='detailed', + action='store_true', + help='Detailed output for tests', + ) + parser.add_argument( + '-l', + '--log', + dest='save_log', + action='store_true', + help='Save unit.log after the test execution', + ) + + return parser.parse_known_args() + + @staticmethod + def _set_args(args): + TestUnit.detailed = args.detailed + TestUnit.save_log = args.save_log + + def _print_path_to_log(self): + print('Path to unit.log:\n' + self.testdir + '/unit.log') |