diff options
Diffstat (limited to 'test')
59 files changed, 1385 insertions, 736 deletions
diff --git a/test/conftest.py b/test/conftest.py index b62264ca..3edc471d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,11 +4,13 @@ import platform import re import shutil import signal +import socket import stat import subprocess import sys import tempfile import time +from multiprocessing import Process import pytest @@ -45,6 +47,7 @@ def pytest_addoption(parser): unit_instance = {} +_processes = [] option = None @@ -66,9 +69,15 @@ def pytest_configure(config): fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) +def skip_alert(*alerts): + option.skip_alerts.extend(alerts) + + def pytest_generate_tests(metafunc): cls = metafunc.cls - if not hasattr(cls, 'application_type'): + if (not hasattr(cls, 'application_type') + or cls.application_type == None + or cls.application_type == 'external'): return type = cls.application_type @@ -127,16 +136,15 @@ def pytest_sessionstart(session): break if m is None: - _print_log() + _print_log(log) exit("Unit is writing log too long") # discover available modules from unit.log for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): - if module[0] not in option.available['modules']: - option.available['modules'][module[0]] = [module[1]] - else: - option.available['modules'][module[0]].append(module[1]) + versions = option.available['modules'].setdefault(module[0], []) + if module[1] not in versions: + versions.append(module[1]) # discover modules from check @@ -154,8 +162,26 @@ def pytest_sessionstart(session): unit_stop() + shutil.rmtree(unit_instance['temp_dir']) + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + # execute all other hooks to obtain the report object + outcome = yield + rep = outcome.get_result() + + # set a report attribute for each phase of a call, which can + # be "setup", "call", "teardown" + + setattr(item, "rep_" + rep.when, rep) + + +@pytest.fixture(autouse=True) +def run(request): + unit = unit_run() + option.temp_dir = unit['temp_dir'] -def setup_method(self): option.skip_alerts = [ r'read signalfd\(4\) failed', r'sendmsg.+failed', @@ -163,6 +189,40 @@ def setup_method(self): ] option.skip_sanitizer = False + yield + + # stop unit + + error = unit_stop() + + if error: + _print_log() + + assert error is None, 'stop unit' + + # stop all processes + + error = stop_processes() + + if error: + _print_log() + + assert error is None, 'stop unit' + + # check unit.log for alerts + + _check_alerts() + + # print unit.log in case of error + + if hasattr(request.node, 'rep_call') and request.node.rep_call.failed: + _print_log() + + # remove unit.log + + if not option.save_log: + shutil.rmtree(unit['temp_dir']) + def unit_run(): global unit_instance build_dir = option.current_dir + '/build' @@ -204,14 +264,6 @@ def unit_run(): _print_log() exit('Could not start unit') - # dumb (TODO: remove) - option.skip_alerts = [ - r'read signalfd\(4\) failed', - r'sendmsg.+failed', - r'recvmsg.+failed', - ] - option.skip_sanitizer = False - unit_instance['temp_dir'] = temp_dir unit_instance['log'] = temp_dir + '/unit.log' unit_instance['control_sock'] = temp_dir + '/control.unit.sock' @@ -232,12 +284,15 @@ def unit_stop(): retcode = p.wait(15) if retcode: return 'Child process terminated with code ' + str(retcode) + + except KeyboardInterrupt: + p.kill() + raise + except: p.kill() return 'Could not terminate unit' - shutil.rmtree(unit_instance['temp_dir']) - def public_dir(path): os.chmod(path, 0o777) @@ -267,11 +322,14 @@ def waitforfiles(*files): return ret -def skip_alert(*alerts): - option.skip_alerts.extend(alerts) +def _check_alerts(path=None): + if path is None: + path = unit_instance['log'] + + with open(path, 'r', encoding='utf-8', errors='ignore') as f: + log = f.read() -def _check_alerts(log): found = False alerts = re.findall(r'.+\[alert\].+', log) @@ -286,23 +344,22 @@ def _check_alerts(log): alerts = [al for al in alerts if re.search(skip, al) is None] if alerts: - _print_log(data=log) + _print_log(log) assert not alerts, 'alert(s)' if not option.skip_sanitizer: sanitizer_errors = re.findall('.+Sanitizer.+', log) if sanitizer_errors: - _print_log(data=log) + _print_log(log) assert not sanitizer_errors, 'sanitizer error(s)' if found: print('skipped.') -def _print_log(path=None, data=None): - if path is None: - path = unit_instance['log'] +def _print_log(data=None): + path = unit_instance['log'] print('Path to unit.log:\n' + path + '\n') @@ -317,6 +374,56 @@ def _print_log(path=None, data=None): sys.stdout.write(data) +def run_process(target, *args): + global _processes + + process = Process(target=target, args=args) + process.start() + + _processes.append(process) + +def stop_processes(): + if not _processes: + return + + fail = False + for process in _processes: + if process.is_alive(): + process.terminate() + process.join(timeout=15) + + if process.is_alive(): + fail = True + + if fail: + return 'Fail to stop process(es)' + + +def waitforsocket(port): + ret = False + + for i in range(50): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', port)) + ret = True + break + + except KeyboardInterrupt: + raise + + except: + sock.close() + time.sleep(0.1) + + sock.close() + + assert ret, 'socket connected' + +@pytest.fixture +def temp_dir(request): + return unit_instance['temp_dir'] + @pytest.fixture def is_unsafe(request): return request.config.getoption("--unsafe") diff --git a/test/go/ns_inspect/app.go b/test/go/ns_inspect/app.go index 4d19a796..570580e6 100644 --- a/test/go/ns_inspect/app.go +++ b/test/go/ns_inspect/app.go @@ -7,6 +7,7 @@ import ( "unit.nginx.org/go" "os" "strconv" + "io/ioutil" ) type ( @@ -26,6 +27,7 @@ type ( GID int NS NS FileExists bool + Mounts string } ) @@ -77,6 +79,11 @@ func handler(w http.ResponseWriter, r *http.Request) { out.FileExists = err == nil } + if mounts := r.Form.Get("mounts"); mounts != "" { + data, _ := ioutil.ReadFile("/proc/self/mountinfo") + out.Mounts = string(data) + } + data, err := json.Marshal(out) if err != nil { w.WriteHeader(http.StatusInternalServerError) diff --git a/test/java/threads/app.java b/test/java/threads/app.java new file mode 100644 index 00000000..d0dd3fcc --- /dev/null +++ b/test/java/threads/app.java @@ -0,0 +1,32 @@ + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet("/") +public class app extends HttpServlet +{ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + int delay = 0; + + String x_delay = request.getHeader("X-Delay"); + if (x_delay != null) { + delay = Integer.parseInt(x_delay); + } + + try { + Thread.sleep(delay * 1000); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + + response.addHeader("X-Thread", "" + Thread.currentThread().getId()); + } +} diff --git a/test/perl/threads/psgi.pl b/test/perl/threads/psgi.pl new file mode 100644 index 00000000..dce28f7d --- /dev/null +++ b/test/perl/threads/psgi.pl @@ -0,0 +1,11 @@ +my $app = sub { + my ($environ) = @_; + + sleep int($environ->{'HTTP_X_DELAY'}); + + return ['200', [ + 'Content-Length' => 0, + 'Psgi-Multithread' => $environ->{'psgi.multithread'}, + 'X-Thread' => $environ->{'psgi.input'} + ], []]; +}; diff --git a/test/php/fastcgi_finish_request/index.php b/test/php/fastcgi_finish_request/index.php new file mode 100644 index 00000000..a6211303 --- /dev/null +++ b/test/php/fastcgi_finish_request/index.php @@ -0,0 +1,11 @@ +<?php +if (!isset($_GET['skip'])) { + echo "0123"; +} + +if (!fastcgi_finish_request()) { + error_log("Error in fastcgi_finish_request"); +} + +echo "4567"; +?> diff --git a/test/pytest.ini b/test/pytest.ini index c672788a..fe86cef2 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = -rs -vvv +addopts = -vvv -s --print_log python_functions = test_* diff --git a/test/python/header_fields/wsgi.py b/test/python/header_fields/wsgi.py new file mode 100644 index 00000000..bd1ba0e2 --- /dev/null +++ b/test/python/header_fields/wsgi.py @@ -0,0 +1,9 @@ +def application(environ, start_response): + + h = (k for k, v in environ.items() if k.startswith('HTTP_')) + + start_response('200', [ + ('Content-Length', '0'), + ('All-Headers', ','.join(h)) + ]) + return [] diff --git a/test/python/legacy/asgi.py b/test/python/legacy/asgi.py new file mode 100644 index 00000000..f065d026 --- /dev/null +++ b/test/python/legacy/asgi.py @@ -0,0 +1,13 @@ +def application(scope): + assert scope['type'] == 'http' + + return app_http + +async def app_http(receive, send): + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + ] + }) diff --git a/test/python/legacy_force/asgi.py b/test/python/legacy_force/asgi.py new file mode 100644 index 00000000..2e5859f2 --- /dev/null +++ b/test/python/legacy_force/asgi.py @@ -0,0 +1,17 @@ +def application(scope, receive=None, send=None): + assert scope['type'] == 'http' + + if receive == None and send == None: + return app_http + + else: + return app_http(receive, send) + +async def app_http(receive, send): + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + ] + }) diff --git a/test/python/threads/asgi.py b/test/python/threads/asgi.py new file mode 100644 index 00000000..d51ae431 --- /dev/null +++ b/test/python/threads/asgi.py @@ -0,0 +1,27 @@ +import asyncio +import time +import threading + +async def application(scope, receive, send): + assert scope['type'] == 'http' + + headers = scope.get('headers', []) + + def get_header(n, v=None): + for h in headers: + if h[0] == n: + return h[1] + return v + + delay = float(get_header(b'x-delay', 0)) + + time.sleep(delay) + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'x-thread', str(threading.currentThread().ident).encode()), + ] + }) diff --git a/test/python/threads/wsgi.py b/test/python/threads/wsgi.py new file mode 100644 index 00000000..1cc8ffe2 --- /dev/null +++ b/test/python/threads/wsgi.py @@ -0,0 +1,15 @@ +import time +import threading + +def application(environ, start_response): + delay = float(environ.get('HTTP_X_DELAY', 0)) + + time.sleep(delay) + + start_response('200', [ + ('Content-Length', '0'), + ('Wsgi-Multithread', str(environ['wsgi.multithread'])), + ('X-Thread', str(threading.currentThread().ident)) + ]) + + return [] diff --git a/test/ruby/threads/config.ru b/test/ruby/threads/config.ru new file mode 100644 index 00000000..2a234d0d --- /dev/null +++ b/test/ruby/threads/config.ru @@ -0,0 +1,13 @@ +app = Proc.new do |env| + delay = env['HTTP_X_DELAY'].to_f + + sleep(delay) + + ['200', { + 'Content-Length' => 0.to_s, + 'Rack-Multithread' => env['rack.multithread'].to_s, + 'X-Thread' => Thread.current.object_id.to_s + }, []] +end + +run app diff --git a/test/test_access_log.py b/test/test_access_log.py index eaba82ab..511ce6c5 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -2,6 +2,8 @@ import time import pytest +from conftest import option +from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython @@ -12,7 +14,7 @@ class TestAccessLog(TestApplicationPython): super().load(script) assert 'success' in self.conf( - '"' + self.temp_dir + '/access.log"', 'access_log' + '"' + option.temp_dir + '/access.log"', 'access_log' ), 'access_log configure' def wait_for_record(self, pattern, name='access.log'): @@ -48,7 +50,7 @@ class TestAccessLog(TestApplicationPython): body='0123456789', ) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"POST / HTTP/1.1" 200 10') is not None @@ -76,7 +78,7 @@ Connection: close raw=True, ) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"') @@ -98,7 +100,7 @@ Connection: close self.get(sock_type='ipv6') - self.stop() + unit_stop() assert ( self.wait_for_record( @@ -110,7 +112,7 @@ Connection: close def test_access_log_unix(self): self.load('empty') - addr = self.temp_dir + '/sock' + addr = option.temp_dir + '/sock' self.conf( {"unix:" + addr: {"pass": "applications/empty"}}, 'listeners' @@ -118,7 +120,7 @@ Connection: close self.get(sock_type='unix', addr=addr) - self.stop() + unit_stop() assert ( self.wait_for_record( @@ -138,7 +140,7 @@ Connection: close } ) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "referer-value" "-"') @@ -156,7 +158,7 @@ Connection: close } ) - self.stop() + unit_stop() assert ( self.wait_for_record( @@ -170,7 +172,7 @@ Connection: close self.get(http_10=True) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"') is not None @@ -185,7 +187,7 @@ Connection: close time.sleep(1) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GE" 400 0 "-" "-"') is not None @@ -198,7 +200,7 @@ Connection: close self.http(b"""GET /\n""", raw=True) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET /" 400 \d+ "-" "-"') is not None @@ -213,7 +215,7 @@ Connection: close time.sleep(1) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET /" 400 0 "-" "-"') is not None @@ -228,7 +230,7 @@ Connection: close time.sleep(1) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET / HTTP/1.1" 400 0 "-" "-"') is not None @@ -242,7 +244,7 @@ Connection: close self.get(headers={'Connection': 'close'}) - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"') @@ -254,7 +256,7 @@ Connection: close self.get(url='/?blah&var=val') - self.stop() + unit_stop() assert ( self.wait_for_record( @@ -270,20 +272,20 @@ Connection: close self.get(url='/delete') - self.stop() + unit_stop() assert self.search_in_log(r'/delete', 'access.log') is None, 'delete' - def test_access_log_change(self): + def test_access_log_change(self, temp_dir): self.load('empty') self.get() - self.conf('"' + self.temp_dir + '/new.log"', 'access_log') + self.conf('"' + option.temp_dir + '/new.log"', 'access_log') self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log') diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index 948d9823..e90d78bc 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -4,6 +4,7 @@ from distutils.version import LooseVersion import pytest +from conftest import option from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython @@ -14,7 +15,7 @@ class TestASGIApplication(TestApplicationPython): load_module = 'asgi' def findall(self, pattern): - with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: + with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def test_asgi_application_variables(self): @@ -136,23 +137,17 @@ custom-header: BLAH ), '204 header transfer encoding' def test_asgi_application_shm_ack_handle(self): - self.load('mirror') - # Minimum possible limit shm_limit = 10 * 1024 * 1024 - assert ( - 'success' in self.conf('{"shm": ' + str(shm_limit) + '}', - 'applications/mirror/limits') - ) + self.load('mirror', limits={"shm": shm_limit}) # Should exceed shm_limit max_body_size = 12 * 1024 * 1024 - assert ( - 'success' in self.conf('{"http":{"max_body_size": ' - + str(max_body_size) + ' }}', - 'settings') + assert 'success' in self.conf( + '{"http":{"max_body_size": ' + str(max_body_size) + ' }}', + 'settings' ) assert self.get()['status'] == 200, 'init' @@ -203,11 +198,6 @@ custom-header: BLAH assert resp['body'] == body, 'keep-alive 2' def test_asgi_keepalive_reconfigure(self): - skip_alert( - r'pthread_mutex.+failed', - r'failed to apply', - r'process \d+ exited on signal', - ) self.load('mirror') assert self.get()['status'] == 200, 'init' @@ -229,9 +219,8 @@ custom-header: BLAH ) assert resp['body'] == body, 'keep-alive open' - assert 'success' in self.conf( - str(i + 1), 'applications/mirror/processes' - ), 'reconfigure' + + self.load('mirror', processes=i + 1) socks.append(sock) @@ -249,9 +238,8 @@ custom-header: BLAH ) assert resp['body'] == body, 'keep-alive request' - assert 'success' in self.conf( - str(i + 1), 'applications/mirror/processes' - ), 'reconfigure 2' + + self.load('mirror', processes=i + 1) for i in range(conns): resp = self.post( @@ -265,9 +253,8 @@ custom-header: BLAH ) assert resp['body'] == body, 'keep-alive close' - assert 'success' in self.conf( - str(i + 1), 'applications/mirror/processes' - ), 'reconfigure 3' + + self.load('mirror', processes=i + 1) def test_asgi_keepalive_reconfigure_2(self): self.load('mirror') @@ -346,11 +333,7 @@ Connection: close assert resp['status'] == 200, 'reconfigure 3' def test_asgi_process_switch(self): - self.load('delayed') - - assert 'success' in self.conf( - '2', 'applications/delayed/processes' - ), 'configure 2 processes' + self.load('delayed', processes=2) self.get( headers={ @@ -381,9 +364,7 @@ Connection: close def test_asgi_application_loading_error(self): skip_alert(r'Python failed to import module "blah"') - self.load('empty') - - assert 'success' in self.conf('"blah"', 'applications/empty/module') + self.load('empty', module="blah") assert self.get()['status'] == 503, 'loading error' @@ -400,3 +381,66 @@ Connection: close assert ( self.wait_for_record(r'\(5\) Thread: 100') is not None ), 'last thread finished' + + def test_asgi_application_threads(self): + self.load('threads', threads=2) + + socks = [] + + for i in range(2): + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '3', + 'Connection': 'close', + }, + no_recv=True, + start=True, + ) + + socks.append(sock) + + time.sleep(1.0) # required to avoid greedy request reading + + threads = set() + + for sock in socks: + resp = self.recvall(sock).decode('utf-8') + + self.log_in(resp) + + resp = self._resp_to_dict(resp) + + assert resp['status'] == 200, 'status' + + threads.add(resp['headers']['x-thread']) + + sock.close() + + assert len(socks) == len(threads), 'threads differs' + + def test_asgi_application_legacy(self): + self.load('legacy') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Content-Length': '0', + 'Connection': 'close', + }, + ) + + assert resp['status'] == 200, 'status' + + def test_asgi_application_legacy_force(self): + self.load('legacy_force', protocol='asgi') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Content-Length': '0', + 'Connection': 'close', + }, + ) + + assert resp['status'] == 200, 'status' diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py index c37a1aae..3f29c7e7 100644 --- a/test/test_asgi_lifespan.py +++ b/test/test_asgi_lifespan.py @@ -5,6 +5,7 @@ import pytest from conftest import option from conftest import public_dir +from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython @@ -34,7 +35,7 @@ class TestASGILifespan(TestApplicationPython): assert self.get()['status'] == 204 - self.stop() + unit_stop() is_startup = os.path.isfile(startup_path) is_shutdown = os.path.isfile(shutdown_path) diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py index ab49b130..54984526 100644 --- a/test/test_asgi_websockets.py +++ b/test/test_asgi_websockets.py @@ -18,8 +18,6 @@ class TestASGIWebsockets(TestApplicationPython): ws = TestApplicationWebsocket() def setup_method(self): - super().setup_method() - assert 'success' in self.conf( {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' ), 'clear keepalive_interval' diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index 1e7243f6..8c4a6b9c 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -1,9 +1,13 @@ import grp import os import pwd +import shutil import pytest +from conftest import option +from conftest import unit_run +from conftest import unit_stop from unit.applications.lang.go import TestApplicationGo from unit.feature.isolation import TestFeatureIsolation @@ -14,11 +18,17 @@ class TestGoIsolation(TestApplicationGo): @classmethod def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) + check = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.temp_dir) + unit = unit_run() + option.temp_dir = unit['temp_dir'] - return unit if not complete_check else unit.complete() + TestFeatureIsolation().check(option.available, unit['temp_dir']) + + assert unit_stop() is None + shutil.rmtree(unit['temp_dir']) + + return check if not complete_check else check() def unpriv_creds(self): nobody_uid = pwd.getpwnam('nobody').pw_uid @@ -26,21 +36,21 @@ class TestGoIsolation(TestApplicationGo): try: nogroup_gid = grp.getgrnam('nogroup').gr_gid nogroup = 'nogroup' - except: + except KeyError: nogroup_gid = grp.getgrnam('nobody').gr_gid nogroup = 'nobody' return (nobody_uid, nogroup_gid, nogroup) def isolation_key(self, key): - return key in self.available['features']['isolation'].keys() + return key in option.available['features']['isolation'].keys() def test_isolation_values(self): self.load('ns_inspect') obj = self.getjson()['body'] - for ns, ns_value in self.available['features']['isolation'].items(): + for ns, ns_value in option.available['features']['isolation'].items(): if ns.upper() in obj['NS']: assert obj['NS'][ns.upper()] == ns_value, '%s match' % ns @@ -198,7 +208,7 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] # all but user and mnt - allns = list(self.available['features']['isolation'].keys()) + allns = list(option.available['features']['isolation'].keys()) allns.remove('user') allns.remove('mnt') @@ -206,7 +216,7 @@ class TestGoIsolation(TestApplicationGo): if ns.upper() in obj['NS']: assert ( obj['NS'][ns.upper()] - == self.available['features']['isolation'][ns] + == option.available['features']['isolation'][ns] ), ('%s match' % ns) assert obj['NS']['MNT'] != self.isolation.getns('mnt'), 'mnt set' @@ -216,13 +226,23 @@ class TestGoIsolation(TestApplicationGo): if not self.isolation_key('pid'): pytest.skip('pid namespace is not supported') - if not (is_su or self.isolation_key('unprivileged_userns_clone')): - pytest.skip('requires root or unprivileged_userns_clone') + if not is_su: + if not self.isolation_key('unprivileged_userns_clone'): + pytest.skip('unprivileged clone is not available') - self.load( - 'ns_inspect', - isolation={'namespaces': {'pid': True, 'credential': True}}, - ) + if not self.isolation_key('user'): + pytest.skip('user namespace is not supported') + + if not self.isolation_key('mnt'): + pytest.skip('mnt namespace is not supported') + + isolation = {'namespaces': {'pid': True}} + + if not is_su: + isolation['namespaces']['mount'] = True + isolation['namespaces']['credential'] = True + + self.load('ns_inspect', isolation=isolation) obj = self.getjson()['body'] @@ -230,7 +250,7 @@ class TestGoIsolation(TestApplicationGo): def test_isolation_namespace_false(self): self.load('ns_inspect') - allns = list(self.available['features']['isolation'].keys()) + allns = list(option.available['features']['isolation'].keys()) remove_list = ['unprivileged_userns_clone', 'ipc', 'cgroup'] allns = [ns for ns in allns if ns not in remove_list] @@ -256,20 +276,31 @@ class TestGoIsolation(TestApplicationGo): if ns.upper() in obj['NS']: assert ( obj['NS'][ns.upper()] - == self.available['features']['isolation'][ns] + == option.available['features']['isolation'][ns] ), ('%s match' % ns) - def test_go_isolation_rootfs_container(self): - if not self.isolation_key('unprivileged_userns_clone'): - pytest.skip('unprivileged clone is not available') + def test_go_isolation_rootfs_container(self, is_su, temp_dir): + if not is_su: + if not self.isolation_key('unprivileged_userns_clone'): + pytest.skip('unprivileged clone is not available') - if not self.isolation_key('mnt'): - pytest.skip('mnt namespace is not supported') + if not self.isolation_key('user'): + pytest.skip('user namespace is not supported') - isolation = { - 'namespaces': {'mount': True, 'credential': True}, - 'rootfs': self.temp_dir, - } + if not self.isolation_key('mnt'): + pytest.skip('mnt namespace is not supported') + + if not self.isolation_key('pid'): + pytest.skip('pid namespace is not supported') + + isolation = {'rootfs': temp_dir} + + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } self.load('ns_inspect', isolation=isolation) @@ -280,7 +311,7 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson(url='/?file=/bin/sh')['body'] assert obj['FileExists'] == False, 'file should not exists' - def test_go_isolation_rootfs_container_priv(self, is_su): + def test_go_isolation_rootfs_container_priv(self, is_su, temp_dir): if not is_su: pytest.skip('requires root') @@ -289,7 +320,7 @@ class TestGoIsolation(TestApplicationGo): isolation = { 'namespaces': {'mount': True}, - 'rootfs': self.temp_dir, + 'rootfs': temp_dir, } self.load('ns_inspect', isolation=isolation) @@ -301,20 +332,50 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson(url='/?file=/bin/sh')['body'] assert obj['FileExists'] == False, 'file should not exists' - def test_go_isolation_rootfs_default_tmpfs(self): - if not self.isolation_key('unprivileged_userns_clone'): - pytest.skip('unprivileged clone is not available') + def test_go_isolation_rootfs_automount_tmpfs(self, is_su, temp_dir): + try: + open("/proc/self/mountinfo") + except: + pytest.skip('The system lacks /proc/self/mountinfo file') - if not self.isolation_key('mnt'): - pytest.skip('mnt namespace is not supported') + if not is_su: + if not self.isolation_key('unprivileged_userns_clone'): + pytest.skip('unprivileged clone is not available') - isolation = { - 'namespaces': {'mount': True, 'credential': True}, - 'rootfs': self.temp_dir, + if not self.isolation_key('user'): + pytest.skip('user namespace is not supported') + + if not self.isolation_key('mnt'): + pytest.skip('mnt namespace is not supported') + + if not self.isolation_key('pid'): + pytest.skip('pid namespace is not supported') + + isolation = {'rootfs': temp_dir} + + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } + + self.load('ns_inspect', isolation=isolation) + + obj = self.getjson(url='/?mounts=true')['body'] + + assert ( + "/ /tmp" in obj['Mounts'] and "tmpfs" in obj['Mounts'] + ), 'app has /tmp mounted on /' + + isolation['automount'] = { + 'tmpfs': False } self.load('ns_inspect', isolation=isolation) - obj = self.getjson(url='/?file=/tmp')['body'] + obj = self.getjson(url='/?mounts=true')['body'] - assert obj['FileExists'] == True, 'app has /tmp' + assert ( + "/ /tmp" not in obj['Mounts'] and "tmpfs" not in obj['Mounts'] + ), 'app has no /tmp mounted' diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py index d8e177b1..1cc59c67 100644 --- a/test/test_go_isolation_rootfs.py +++ b/test/test_go_isolation_rootfs.py @@ -8,7 +8,7 @@ from unit.applications.lang.go import TestApplicationGo class TestGoIsolationRootfs(TestApplicationGo): prerequisites = {'modules': {'go': 'all'}} - def test_go_isolation_rootfs_chroot(self, is_su): + def test_go_isolation_rootfs_chroot(self, is_su, temp_dir): if not is_su: pytest.skip('requires root') @@ -16,7 +16,7 @@ class TestGoIsolationRootfs(TestApplicationGo): pytest.skip('chroot tests not supported on OSX') isolation = { - 'rootfs': self.temp_dir, + 'rootfs': temp_dir, } self.load('ns_inspect', isolation=isolation) diff --git a/test/test_http_header.py b/test/test_http_header.py index 8381a0d9..fdb557cf 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -154,54 +154,58 @@ Connection: close def test_http_header_field_leading_sp(self): self.load('empty') - resp = self.get( - headers={ - 'Host': 'localhost', - ' Custom-Header': 'blah', - 'Connection': 'close', - } - ) - - assert resp['status'] == 400, 'field leading sp' + assert ( + self.get( + headers={ + 'Host': 'localhost', + ' Custom-Header': 'blah', + 'Connection': 'close', + } + )['status'] + == 400 + ), 'field leading sp' def test_http_header_field_leading_htab(self): self.load('empty') - resp = self.get( - headers={ - 'Host': 'localhost', - '\tCustom-Header': 'blah', - 'Connection': 'close', - } - ) - - assert resp['status'] == 400, 'field leading htab' + assert ( + self.get( + headers={ + 'Host': 'localhost', + '\tCustom-Header': 'blah', + 'Connection': 'close', + } + )['status'] + == 400 + ), 'field leading htab' def test_http_header_field_trailing_sp(self): self.load('empty') - resp = self.get( - headers={ - 'Host': 'localhost', - 'Custom-Header ': 'blah', - 'Connection': 'close', - } - ) - - assert resp['status'] == 400, 'field trailing sp' + assert ( + self.get( + headers={ + 'Host': 'localhost', + 'Custom-Header ': 'blah', + 'Connection': 'close', + } + )['status'] + == 400 + ), 'field trailing sp' def test_http_header_field_trailing_htab(self): self.load('empty') - resp = self.get( - headers={ - 'Host': 'localhost', - 'Custom-Header\t': 'blah', - 'Connection': 'close', - } - ) - - assert resp['status'] == 400, 'field trailing htab' + assert ( + self.get( + headers={ + 'Host': 'localhost', + 'Custom-Header\t': 'blah', + 'Connection': 'close', + } + )['status'] + == 400 + ), 'field trailing htab' def test_http_header_content_length_big(self): self.load('empty') @@ -427,3 +431,41 @@ Connection: close )['status'] == 400 ), 'Host multiple fields' + + def test_http_discard_unsafe_fields(self): + self.load('header_fields') + + def check_status(header): + resp = self.get( + headers={ + 'Host': 'localhost', + header: 'blah', + 'Connection': 'close', + } + ) + + assert resp['status'] == 200 + return resp + + resp = check_status("!Custom-Header") + assert 'CUSTOM' not in resp['headers']['All-Headers'] + + resp = check_status("Custom_Header") + assert 'CUSTOM' not in resp['headers']['All-Headers'] + + assert 'success' in self.conf( + {'http': {'discard_unsafe_fields': False}}, 'settings', + ) + + resp = check_status("!#$%&'*+.^`|~Custom_Header") + assert 'CUSTOM' in resp['headers']['All-Headers'] + + assert 'success' in self.conf( + {'http': {'discard_unsafe_fields': True}}, 'settings', + ) + + resp = check_status("!Custom-Header") + assert 'CUSTOM' not in resp['headers']['All-Headers'] + + resp = check_status("Custom_Header") + assert 'CUSTOM' not in resp['headers']['All-Headers'] diff --git a/test/test_java_application.py b/test/test_java_application.py index afcdf651..41345e87 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -11,7 +11,7 @@ from unit.applications.lang.java import TestApplicationJava class TestJavaApplication(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} - def test_java_conf_error(self): + def test_java_conf_error(self, temp_dir): skip_alert( r'realpath.*failed', r'failed to apply new conf', @@ -25,18 +25,18 @@ class TestJavaApplication(TestApplicationJava): "type": "java", "processes": 1, "working_directory": option.test_dir + "/java/empty", - "webapp": self.temp_dir + "/java", - "unit_jars": self.temp_dir + "/no_such_dir", + "webapp": temp_dir + "/java", + "unit_jars": temp_dir + "/no_such_dir", } }, } ), 'conf error' - def test_java_war(self): + def test_java_war(self, temp_dir): self.load('empty_war') assert 'success' in self.conf( - '"' + self.temp_dir + '/java/empty.war"', + '"' + temp_dir + '/java/empty.war"', '/config/applications/empty_war/webapp', ), 'configure war' @@ -969,11 +969,11 @@ class TestJavaApplication(TestApplicationJava): ), 'set date header' assert headers['X-Get-Date'] == date, 'get date header' - def test_java_application_multipart(self): + def test_java_application_multipart(self, temp_dir): self.load('multipart') reldst = '/uploads' - fulldst = self.temp_dir + reldst + fulldst = temp_dir + reldst os.mkdir(fulldst) public_dir(fulldst) @@ -1012,3 +1012,44 @@ class TestJavaApplication(TestApplicationJava): ) is not None ), 'file created' + + def test_java_application_threads(self): + self.load('threads') + + assert 'success' in self.conf( + '4', 'applications/threads/threads' + ), 'configure 4 threads' + + socks = [] + + for i in range(4): + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '2', + 'Connection': 'close', + }, + no_recv=True, + start=True, + ) + + socks.append(sock) + + time.sleep(0.25) # required to avoid greedy request reading + + threads = set() + + for sock in socks: + resp = self.recvall(sock).decode('utf-8') + + self.log_in(resp) + + resp = self._resp_to_dict(resp) + + assert resp['status'] == 200, 'status' + + threads.add(resp['headers']['X-Thread']) + + sock.close() + + assert len(socks) == len(threads), 'threads differs' diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index f0f04df1..02d35a62 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -11,14 +11,12 @@ class TestJavaIsolationRootfs(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} def setup_method(self, is_su): - super().setup_method() - if not is_su: return - os.makedirs(self.temp_dir + '/jars') - os.makedirs(self.temp_dir + '/tmp') - os.chmod(self.temp_dir + '/tmp', 0o777) + os.makedirs(option.temp_dir + '/jars') + os.makedirs(option.temp_dir + '/tmp') + os.chmod(option.temp_dir + '/tmp', 0o777) try: process = subprocess.Popen( @@ -26,15 +24,18 @@ class TestJavaIsolationRootfs(TestApplicationJava): "mount", "--bind", option.current_dir + "/build", - self.temp_dir + "/jars", + option.temp_dir + "/jars", ], stderr=subprocess.STDOUT, ) process.communicate() + except KeyboardInterrupt: + raise + except: - pytest.fail('Cann\'t run mount process.') + pytest.fail('Can\'t run mount process.') def teardown_method(self, is_su): if not is_su: @@ -42,24 +43,24 @@ class TestJavaIsolationRootfs(TestApplicationJava): try: process = subprocess.Popen( - ["umount", "--lazy", self.temp_dir + "/jars"], + ["umount", "--lazy", option.temp_dir + "/jars"], stderr=subprocess.STDOUT, ) process.communicate() - except: - pytest.fail('Cann\'t run mount process.') + except KeyboardInterrupt: + raise - # super teardown must happen after unmount to avoid deletion of /build - super().teardown_method() + except: + pytest.fail('Can\'t run mount process.') - def test_java_isolation_rootfs_chroot_war(self, is_su): + def test_java_isolation_rootfs_chroot_war(self, is_su, temp_dir): if not is_su: pytest.skip('require root') isolation = { - 'rootfs': self.temp_dir, + 'rootfs': temp_dir, } self.load('empty_war', isolation=isolation) diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py index 7e6d82e8..7586d4aa 100644 --- a/test/test_java_websockets.py +++ b/test/test_java_websockets.py @@ -15,8 +15,6 @@ class TestJavaWebsockets(TestApplicationJava): ws = TestApplicationWebsocket() def setup_method(self): - super().setup_method() - assert 'success' in self.conf( {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' ), 'clear keepalive_interval' diff --git a/test/test_node_application.py b/test/test_node_application.py index a0b882f3..c8c3a444 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -139,11 +139,11 @@ class TestNodeApplication(TestApplicationNode): assert self.get()['body'] == 'buffer', 'write buffer' - def test_node_application_write_callback(self): + def test_node_application_write_callback(self, temp_dir): self.load('write_callback') assert self.get()['body'] == 'helloworld', 'write callback order' - assert waitforfiles(self.temp_dir + '/node/callback'), 'write callback' + assert waitforfiles(temp_dir + '/node/callback'), 'write callback' def test_node_application_write_before_write_head(self): self.load('write_before_write_head') @@ -222,7 +222,7 @@ class TestNodeApplication(TestApplicationNode): assert 'X-Header' not in headers, 'insensitive' assert 'X-header' not in headers, 'insensitive 2' - def test_node_application_promise_handler(self): + def test_node_application_promise_handler(self, temp_dir): self.load('promise_handler') assert ( @@ -236,7 +236,7 @@ class TestNodeApplication(TestApplicationNode): )['status'] == 200 ), 'promise handler request' - assert waitforfiles(self.temp_dir + '/node/callback'), 'promise handler' + assert waitforfiles(temp_dir + '/node/callback'), 'promise handler' def test_node_application_promise_handler_write_after_end(self): self.load('promise_handler') @@ -254,7 +254,7 @@ class TestNodeApplication(TestApplicationNode): == 200 ), 'promise handler request write after end' - def test_node_application_promise_end(self): + def test_node_application_promise_end(self, temp_dir): self.load('promise_end') assert ( @@ -268,9 +268,9 @@ class TestNodeApplication(TestApplicationNode): )['status'] == 200 ), 'promise end request' - assert waitforfiles(self.temp_dir + '/node/callback'), 'promise end' + assert waitforfiles(temp_dir + '/node/callback'), 'promise end' - def test_node_application_promise_multiple_calls(self): + def test_node_application_promise_multiple_calls(self, temp_dir): self.load('promise_handler') self.post( @@ -283,7 +283,7 @@ class TestNodeApplication(TestApplicationNode): ) assert waitforfiles( - self.temp_dir + '/node/callback1' + temp_dir + '/node/callback1' ), 'promise first call' self.post( @@ -296,7 +296,7 @@ class TestNodeApplication(TestApplicationNode): ) assert waitforfiles( - self.temp_dir + '/node/callback2' + temp_dir + '/node/callback2' ), 'promise second call' @pytest.mark.skip('not yet') diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py index 6a6b7f2d..7b65b5c1 100644 --- a/test/test_node_websockets.py +++ b/test/test_node_websockets.py @@ -15,8 +15,6 @@ class TestNodeWebsockets(TestApplicationNode): ws = TestApplicationWebsocket() def setup_method(self): - super().setup_method() - assert 'success' in self.conf( {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' ), 'clear keepalive_interval' diff --git a/test/test_perl_application.py b/test/test_perl_application.py index 78e32a43..78f2dd90 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -3,6 +3,7 @@ import re import pytest from conftest import skip_alert +from conftest import unit_stop from unit.applications.lang.perl import TestApplicationPerl @@ -119,7 +120,7 @@ class TestPerlApplication(TestApplicationPerl): assert self.get()['body'] == '1', 'errors result' - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+Error in application') @@ -237,3 +238,44 @@ class TestPerlApplication(TestApplicationPerl): assert resp['status'] == 200, 'status' assert resp['body'] == 'Hello World!', 'body' + + def test_perl_application_threads(self): + self.load('threads') + + assert 'success' in self.conf( + '4', 'applications/threads/threads' + ), 'configure 4 threads' + + socks = [] + + for i in range(4): + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '2', + 'Connection': 'close', + }, + no_recv=True, + start=True, + ) + + socks.append(sock) + + threads = set() + + for sock in socks: + resp = self.recvall(sock).decode('utf-8') + + self.log_in(resp) + + resp = self._resp_to_dict(resp) + + assert resp['status'] == 200, 'status' + + threads.add(resp['headers']['X-Thread']) + + assert resp['headers']['Psgi-Multithread'] == '1', 'multithread' + + sock.close() + + assert len(socks) == len(threads), 'threads differs' diff --git a/test/test_php_application.py b/test/test_php_application.py index 063d3e0c..578de0b7 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -6,6 +6,7 @@ import time import pytest from conftest import option +from conftest import unit_stop from unit.applications.lang.php import TestApplicationPHP class TestPHPApplication(TestApplicationPHP): @@ -93,6 +94,32 @@ class TestPHPApplication(TestApplicationPHP): assert resp['status'] == 200, 'query string empty status' assert resp['headers']['Query-String'] == '', 'query string empty' + def test_php_application_fastcgi_finish_request(self, temp_dir): + self.load('fastcgi_finish_request') + + assert self.get()['body'] == '0123' + + unit_stop() + + with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: + errs = re.findall(r'Error in fastcgi_finish_request', f.read()) + + assert len(errs) == 0, 'no error' + + def test_php_application_fastcgi_finish_request_2(self, temp_dir): + self.load('fastcgi_finish_request') + + resp = self.get(url='/?skip') + assert resp['status'] == 200 + assert resp['body'] == '' + + unit_stop() + + with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: + errs = re.findall(r'Error in fastcgi_finish_request', f.read()) + + assert len(errs) == 0, 'no error' + def test_php_application_query_string_absent(self): self.load('query_string') @@ -444,7 +471,7 @@ class TestPHPApplication(TestApplicationPHP): r'012345', self.get()['body'] ), 'disable_classes before' - def test_php_application_error_log(self): + def test_php_application_error_log(self, temp_dir): self.load('error_log') assert self.get()['status'] == 200, 'status' @@ -453,13 +480,13 @@ class TestPHPApplication(TestApplicationPHP): assert self.get()['status'] == 200, 'status 2' - self.stop() + unit_stop() pattern = r'\d{4}\/\d\d\/\d\d\s\d\d:.+\[notice\].+Error in application' assert self.wait_for_record(pattern) is not None, 'errors print' - with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: + with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: errs = re.findall(pattern, f.read()) assert len(errs) == 2, 'error_log count' @@ -507,12 +534,12 @@ class TestPHPApplication(TestApplicationPHP): assert resp['status'] == 200, 'status' assert resp['body'] != '', 'body not empty' - def test_php_application_extension_check(self): + def test_php_application_extension_check(self, temp_dir): self.load('phpinfo') assert self.get(url='/index.wrong')['status'] != 200, 'status' - new_root = self.temp_dir + "/php" + new_root = temp_dir + "/php" os.mkdir(new_root) shutil.copy(option.test_dir + '/php/phpinfo/index.wrong', new_root) diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index 8ab3419a..cc660e04 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -1,6 +1,10 @@ +import shutil + import pytest from conftest import option +from conftest import unit_run +from conftest import unit_stop from unit.applications.lang.php import TestApplicationPHP from unit.feature.isolation import TestFeatureIsolation @@ -8,67 +12,85 @@ from unit.feature.isolation import TestFeatureIsolation class TestPHPIsolation(TestApplicationPHP): prerequisites = {'modules': {'php': 'any'}, 'features': ['isolation']} - isolation = TestFeatureIsolation() - @classmethod def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) + check = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.temp_dir) + unit = unit_run() + option.temp_dir = unit['temp_dir'] - return unit if not complete_check else unit.complete() + TestFeatureIsolation().check(option.available, unit['temp_dir']) - def test_php_isolation_rootfs(self, is_su): - isolation_features = self.available['features']['isolation'].keys() + assert unit_stop() is None + shutil.rmtree(unit['temp_dir']) - if 'mnt' not in isolation_features: - pytest.skip('requires mnt ns') + return check if not complete_check else check() - if not is_su: - if 'user' not in isolation_features: - pytest.skip('requires unprivileged userns or root') + def test_php_isolation_rootfs(self, is_su, temp_dir): + isolation_features = option.available['features']['isolation'].keys() + if not is_su: if not 'unprivileged_userns_clone' in isolation_features: pytest.skip('requires unprivileged userns or root') - isolation = { - 'namespaces': {'credential': not is_su, 'mount': True}, - 'rootfs': option.test_dir, - } + if 'user' not in isolation_features: + pytest.skip('user namespace is not supported') + + if 'mnt' not in isolation_features: + pytest.skip('mnt namespace is not supported') + + if 'pid' not in isolation_features: + pytest.skip('pid namespace is not supported') + + isolation = {'rootfs': temp_dir} + + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } self.load('phpinfo', isolation=isolation) assert 'success' in self.conf( - '"/php/phpinfo"', 'applications/phpinfo/root' + '"/app/php/phpinfo"', 'applications/phpinfo/root' ) assert 'success' in self.conf( - '"/php/phpinfo"', 'applications/phpinfo/working_directory' + '"/app/php/phpinfo"', 'applications/phpinfo/working_directory' ) assert self.get()['status'] == 200, 'empty rootfs' - def test_php_isolation_rootfs_extensions(self, is_su): - isolation_features = self.available['features']['isolation'].keys() + def test_php_isolation_rootfs_extensions(self, is_su, temp_dir): + isolation_features = option.available['features']['isolation'].keys() if not is_su: - if 'user' not in isolation_features: - pytest.skip('requires unprivileged userns or root') - if not 'unprivileged_userns_clone' in isolation_features: pytest.skip('requires unprivileged userns or root') + if 'user' not in isolation_features: + pytest.skip('user namespace is not supported') + if 'mnt' not in isolation_features: - pytest.skip('requires mnt ns') + pytest.skip('mnt namespace is not supported') + + if 'pid' not in isolation_features: + pytest.skip('pid namespace is not supported') - isolation = { - 'rootfs': option.test_dir, - 'namespaces': {'credential': not is_su, 'mount': not is_su}, - } + isolation = {'rootfs': temp_dir} + + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } self.load('list-extensions', isolation=isolation) assert 'success' in self.conf( - '"/php/list-extensions"', 'applications/list-extensions/root' + '"/app/php/list-extensions"', 'applications/list-extensions/root' ) assert 'success' in self.conf( @@ -77,7 +99,7 @@ class TestPHPIsolation(TestApplicationPHP): ) assert 'success' in self.conf( - '"/php/list-extensions"', + '"/app/php/list-extensions"', 'applications/list-extensions/working_directory', ) diff --git a/test/test_proxy.py b/test/test_proxy.py index d02c96a7..be3e93fd 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -5,7 +5,9 @@ import time import pytest from conftest import option +from conftest import run_process from conftest import skip_alert +from conftest import waitforsocket from unit.applications.lang.python import TestApplicationPython @@ -60,10 +62,8 @@ Content-Length: 10 return self.post(*args, http_10=True, **kwargs) def setup_method(self): - super().setup_method() - - self.run_process(self.run_server, self.SERVER_PORT) - self.waitforsocket(self.SERVER_PORT) + run_process(self.run_server, self.SERVER_PORT) + waitforsocket(self.SERVER_PORT) assert 'success' in self.conf( { @@ -346,8 +346,8 @@ Content-Length: 10 assert self.get_http10()['status'] == 200, 'status' - def test_proxy_unix(self): - addr = self.temp_dir + '/sock' + def test_proxy_unix(self, temp_dir): + addr = temp_dir + '/sock' assert 'success' in self.conf( { diff --git a/test/test_proxy_chunked.py b/test/test_proxy_chunked.py index 26023617..ae2228fa 100644 --- a/test/test_proxy_chunked.py +++ b/test/test_proxy_chunked.py @@ -3,6 +3,9 @@ import select import socket import time +from conftest import option +from conftest import run_process +from conftest import waitforsocket from unit.applications.lang.python import TestApplicationPython @@ -82,10 +85,8 @@ class TestProxyChunked(TestApplicationPython): return self.get(*args, http_10=True, **kwargs) def setup_method(self): - super().setup_method() - - self.run_process(self.run_server, self.SERVER_PORT, self.temp_dir) - self.waitforsocket(self.SERVER_PORT) + run_process(self.run_server, self.SERVER_PORT, option.temp_dir) + waitforsocket(self.SERVER_PORT) assert 'success' in self.conf( { diff --git a/test/test_python_application.py b/test/test_python_application.py index 3e27a24c..83b0c8f4 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -5,7 +5,9 @@ import time import pytest +from conftest import option from conftest import skip_alert +from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython @@ -13,7 +15,7 @@ class TestPythonApplication(TestApplicationPython): prerequisites = {'modules': {'python': 'all'}} def findall(self, pattern): - with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: + with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def test_python_application_variables(self): @@ -156,7 +158,7 @@ custom-header: BLAH self.conf({"listeners": {}, "applications": {}}) - self.stop() + unit_stop() assert ( self.wait_for_record(r'RuntimeError') is not None @@ -195,11 +197,6 @@ custom-header: BLAH assert resp['body'] == body, 'keep-alive 2' def test_python_keepalive_reconfigure(self): - skip_alert( - r'pthread_mutex.+failed', - r'failed to apply', - r'process \d+ exited on signal', - ) self.load('mirror') assert self.get()['status'] == 200, 'init' @@ -221,9 +218,8 @@ custom-header: BLAH ) assert resp['body'] == body, 'keep-alive open' - assert 'success' in self.conf( - str(i + 1), 'applications/mirror/processes' - ), 'reconfigure' + + self.load('mirror', processes=i + 1) socks.append(sock) @@ -241,9 +237,8 @@ custom-header: BLAH ) assert resp['body'] == body, 'keep-alive request' - assert 'success' in self.conf( - str(i + 1), 'applications/mirror/processes' - ), 'reconfigure 2' + + self.load('mirror', processes=i + 1) for i in range(conns): resp = self.post( @@ -257,9 +252,8 @@ custom-header: BLAH ) assert resp['body'] == body, 'keep-alive close' - assert 'success' in self.conf( - str(i + 1), 'applications/mirror/processes' - ), 'reconfigure 3' + + self.load('mirror', processes=i + 1) def test_python_keepalive_reconfigure_2(self): self.load('mirror') @@ -344,16 +338,12 @@ Connection: close self.conf({"listeners": {}, "applications": {}}) - self.stop() + unit_stop() assert self.wait_for_record(r'At exit called\.') is not None, 'atexit' def test_python_process_switch(self): - self.load('delayed') - - assert 'success' in self.conf( - '2', 'applications/delayed/processes' - ), 'configure 2 processes' + self.load('delayed', processes=2) self.get( headers={ @@ -507,7 +497,7 @@ last line: 987654321 self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+Error in application\.') @@ -539,9 +529,7 @@ last line: 987654321 def test_python_application_loading_error(self): skip_alert(r'Python failed to import module "blah"') - self.load('empty') - - assert 'success' in self.conf('"blah"', 'applications/empty/module') + self.load('empty', module="blah") assert self.get()['status'] == 503, 'loading error' @@ -550,7 +538,7 @@ last line: 987654321 self.get() - self.stop() + unit_stop() assert self.wait_for_record(r'Close called\.') is not None, 'close' @@ -559,7 +547,7 @@ last line: 987654321 self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'Close called\.') is not None @@ -570,7 +558,7 @@ last line: 987654321 self.get() - self.stop() + unit_stop() assert ( self.wait_for_record( @@ -742,7 +730,7 @@ last line: 987654321 try: group_id = grp.getgrnam(group).gr_gid - except: + except KeyError: group = 'nogroup' group_id = grp.getgrnam(group).gr_gid @@ -787,7 +775,7 @@ last line: 987654321 try: grp.getgrnam(group) group = True - except: + except KeyError: group = False if group: @@ -809,24 +797,47 @@ last line: 987654321 assert self.get()['status'] == 204, 'default application response' - assert 'success' in self.conf( - '"app"', 'applications/callable/callable' - ) + self.load('callable', callable="app") assert self.get()['status'] == 200, 'callable response' - assert 'success' in self.conf( - '"blah"', 'applications/callable/callable' - ) + self.load('callable', callable="blah") assert self.get()['status'] not in [200, 204], 'callable response inv' - assert 'success' in self.conf( - '"app"', 'applications/callable/callable' - ) + def test_python_application_threads(self): + self.load('threads', threads=4) + + socks = [] + + for i in range(4): + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '2', + 'Connection': 'close', + }, + no_recv=True, + start=True, + ) + + socks.append(sock) + + threads = set() + + for sock in socks: + resp = self.recvall(sock).decode('utf-8') + + self.log_in(resp) + + resp = self._resp_to_dict(resp) + + assert resp['status'] == 200, 'status' + + threads.add(resp['headers']['X-Thread']) - assert self.get()['status'] == 200, 'callable response 2' + assert resp['headers']['Wsgi-Multithread'] == 'True', 'multithread' - assert 'success' in self.conf_delete('applications/callable/callable') + sock.close() - assert self.get()['status'] == 204, 'default response 2' + assert len(socks) == len(threads), 'threads differs' diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index ac678103..1a157528 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -1,5 +1,10 @@ +import shutil + import pytest +from conftest import option +from conftest import unit_run +from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython from unit.feature.isolation import TestFeatureIsolation @@ -7,48 +12,55 @@ from unit.feature.isolation import TestFeatureIsolation class TestPythonIsolation(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}, 'features': ['isolation']} - isolation = TestFeatureIsolation() - @classmethod def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) + check = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.temp_dir) + unit = unit_run() + option.temp_dir = unit['temp_dir'] - return unit if not complete_check else unit.complete() + TestFeatureIsolation().check(option.available, unit['temp_dir']) - def test_python_isolation_rootfs(self, is_su): - isolation_features = self.available['features']['isolation'].keys() + assert unit_stop() is None + shutil.rmtree(unit['temp_dir']) - if 'mnt' not in isolation_features: - pytest.skip('requires mnt ns') + return check if not complete_check else check() - if not is_su: - if 'user' not in isolation_features: - pytest.skip('requires unprivileged userns or root') + def test_python_isolation_rootfs(self, is_su, temp_dir): + isolation_features = option.available['features']['isolation'].keys() + if not is_su: if not 'unprivileged_userns_clone' in isolation_features: pytest.skip('requires unprivileged userns or root') - isolation = { - 'namespaces': {'credential': not is_su, 'mount': True}, - 'rootfs': self.temp_dir, - } + if 'user' not in isolation_features: + pytest.skip('user namespace is not supported') - self.load('empty', isolation=isolation) + if 'mnt' not in isolation_features: + pytest.skip('mnt namespace is not supported') + + if 'pid' not in isolation_features: + pytest.skip('pid namespace is not supported') + + isolation = {'rootfs': temp_dir} - assert self.get()['status'] == 200, 'python rootfs' + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } self.load('ns_inspect', isolation=isolation) assert ( - self.getjson(url='/?path=' + self.temp_dir)['body']['FileExists'] + self.getjson(url='/?path=' + temp_dir)['body']['FileExists'] == False ), 'temp_dir does not exists in rootfs' assert ( self.getjson(url='/?path=/proc/self')['body']['FileExists'] - == False + == True ), 'no /proc/self' assert ( @@ -66,25 +78,34 @@ class TestPythonIsolation(TestApplicationPython): ret['body']['FileExists'] == True ), 'application exists in rootfs' - def test_python_isolation_rootfs_no_language_deps(self, is_su): - isolation_features = self.available['features']['isolation'].keys() - - if 'mnt' not in isolation_features: - pytest.skip('requires mnt ns') + def test_python_isolation_rootfs_no_language_deps(self, is_su, temp_dir): + isolation_features = option.available['features']['isolation'].keys() if not is_su: - if 'user' not in isolation_features: - pytest.skip('requires unprivileged userns or root') - if not 'unprivileged_userns_clone' in isolation_features: pytest.skip('requires unprivileged userns or root') + if 'user' not in isolation_features: + pytest.skip('user namespace is not supported') + + if 'mnt' not in isolation_features: + pytest.skip('mnt namespace is not supported') + + if 'pid' not in isolation_features: + pytest.skip('pid namespace is not supported') + isolation = { - 'namespaces': {'credential': not is_su, 'mount': True}, - 'rootfs': self.temp_dir, + 'rootfs': temp_dir, 'automount': {'language_deps': False} } + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } + self.load('empty', isolation=isolation) assert (self.get()['status'] != 200), 'disabled language_deps' diff --git a/test/test_python_isolation_chroot.py b/test/test_python_isolation_chroot.py index 315fee9f..8018d5b9 100644 --- a/test/test_python_isolation_chroot.py +++ b/test/test_python_isolation_chroot.py @@ -7,28 +7,24 @@ from unit.feature.isolation import TestFeatureIsolation class TestPythonIsolation(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def test_python_isolation_chroot(self, is_su): + def test_python_isolation_chroot(self, is_su, temp_dir): if not is_su: pytest.skip('requires root') isolation = { - 'rootfs': self.temp_dir, + 'rootfs': temp_dir, } - self.load('empty', isolation=isolation) - - assert self.get()['status'] == 200, 'python chroot' - self.load('ns_inspect', isolation=isolation) assert ( - self.getjson(url='/?path=' + self.temp_dir)['body']['FileExists'] + self.getjson(url='/?path=' + temp_dir)['body']['FileExists'] == False ), 'temp_dir does not exists in rootfs' assert ( self.getjson(url='/?path=/proc/self')['body']['FileExists'] - == False + == True ), 'no /proc/self' assert ( diff --git a/test/test_python_procman.py b/test/test_python_procman.py index 8eccae3e..ff914fc8 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -4,6 +4,7 @@ import time import pytest +from conftest import option from unit.applications.lang.python import TestApplicationPython @@ -11,9 +12,7 @@ class TestPythonProcman(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} def setup_method(self): - super().setup_method() - - self.app_name = "app-" + self.temp_dir.split('/')[-1] + self.app_name = "app-" + option.temp_dir.split('/')[-1] self.app_proc = 'applications/' + self.app_name + '/processes' self.load('empty', self.app_name) diff --git a/test/test_respawn.py b/test/test_respawn.py index 18b9d535..09a806d4 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -2,6 +2,7 @@ import re import subprocess import time +from conftest import option from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython @@ -13,9 +14,7 @@ class TestRespawn(TestApplicationPython): PATTERN_CONTROLLER = 'unit: controller' def setup_method(self): - super().setup_method() - - self.app_name = "app-" + self.temp_dir.split('/')[-1] + self.app_name = "app-" + option.temp_dir.split('/')[-1] self.load('empty', self.app_name) diff --git a/test/test_return.py b/test/test_return.py index 64050022..2f7b7ae4 100644 --- a/test/test_return.py +++ b/test/test_return.py @@ -7,8 +7,6 @@ class TestReturn(TestApplicationProto): prerequisites = {} def setup_method(self): - super().setup_method() - self._load_conf( { "listeners": {"*:7080": {"pass": "routes"}}, diff --git a/test/test_routing.py b/test/test_routing.py index 2b528435..83852273 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -10,8 +10,6 @@ class TestRouting(TestApplicationProto): prerequisites = {'modules': {'python': 'any'}} def setup_method(self): - super().setup_method() - assert 'success' in self.conf( { "listeners": {"*:7080": {"pass": "routes"}}, @@ -232,6 +230,48 @@ class TestRouting(TestApplicationProto): assert self.get(url='/aBCaBbc')['status'] == 200 assert self.get(url='/ABc')['status'] == 404 + def test_routes_empty_regex(self): + self.route_match({"uri":"~"}) + assert self.get(url='/')['status'] == 200, 'empty regexp' + assert self.get(url='/anything')['status'] == 200, '/anything' + + self.route_match({"uri":"!~"}) + assert self.get(url='/')['status'] == 404, 'empty regexp 2' + assert self.get(url='/nothing')['status'] == 404, '/nothing' + + def test_routes_bad_regex(self): + assert 'error' in self.route( + {"match": {"uri": "~/bl[ah"}, "action": {"return": 200}} + ), 'bad regex' + + status = self.route( + {"match": {"uri": "~(?R)?z"}, "action": {"return": 200}} + ) + if 'error' not in status: + assert self.get(url='/nothing_z')['status'] == 500, '/nothing_z' + + status = self.route( + {"match": {"uri": "~((?1)?z)"}, "action": {"return": 200}} + ) + if 'error' not in status: + assert self.get(url='/nothing_z')['status'] == 500, '/nothing_z' + + def test_routes_match_regex_case_sensitive(self): + self.route_match({"uri": "~/bl[ah]"}) + + assert self.get(url='/rlah')['status'] == 404, '/rlah' + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/blh')['status'] == 200, '/blh' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' + + def test_routes_match_regex_negative_case_sensitive(self): + self.route_match({"uri": "!~/bl[ah]"}) + + assert self.get(url='/rlah')['status'] == 200, '/rlah' + assert self.get(url='/blah')['status'] == 404, '/blah' + assert self.get(url='/blh')['status'] == 404, '/blh' + assert self.get(url='/BLAH')['status'] == 200, '/BLAH' + def test_routes_pass_encode(self): def check_pass(path, name): assert 'success' in self.conf( @@ -417,7 +457,7 @@ class TestRouting(TestApplicationProto): [{"action": {"pass": "upstreams/blah"}}], 'routes' ), 'route pass upstreams invalid' - def test_routes_action_unique(self): + def test_routes_action_unique(self, temp_dir): assert 'success' in self.conf( { "listeners": { @@ -437,7 +477,7 @@ class TestRouting(TestApplicationProto): ) assert 'error' in self.conf( - {"proxy": "http://127.0.0.1:7081", "share": self.temp_dir}, + {"proxy": "http://127.0.0.1:7081", "share": temp_dir}, 'routes/0/action', ), 'proxy share' assert 'error' in self.conf( @@ -445,7 +485,7 @@ class TestRouting(TestApplicationProto): 'routes/0/action', ), 'proxy pass' assert 'error' in self.conf( - {"share": self.temp_dir, "pass": "applications/app"}, + {"share": temp_dir, "pass": "applications/app"}, 'routes/0/action', ), 'share pass' @@ -1665,8 +1705,8 @@ class TestRouting(TestApplicationProto): assert self.get(sock_type='ipv6')['status'] == 200, '0' assert self.get(port=7081)['status'] == 404, '0 ipv4' - def test_routes_source_unix(self): - addr = self.temp_dir + '/sock' + def test_routes_source_unix(self, temp_dir): + addr = temp_dir + '/sock' assert 'success' in self.conf( {"unix:" + addr: {"pass": "routes"}}, 'listeners' diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index f84935f8..e42fb97f 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -3,6 +3,7 @@ import re import pytest from conftest import skip_alert +from conftest import unit_stop from unit.applications.lang.ruby import TestApplicationRuby @@ -175,7 +176,7 @@ class TestRubyApplication(TestApplicationRuby): self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+Error in application') @@ -187,7 +188,7 @@ class TestRubyApplication(TestApplicationRuby): self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+1234567890') is not None @@ -198,7 +199,7 @@ class TestRubyApplication(TestApplicationRuby): self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+Error in application') @@ -215,7 +216,7 @@ class TestRubyApplication(TestApplicationRuby): self.get() - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+1234567890') is not None @@ -228,7 +229,7 @@ class TestRubyApplication(TestApplicationRuby): self.conf({"listeners": {}, "applications": {}}) - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+At exit called\.') is not None @@ -289,7 +290,7 @@ class TestRubyApplication(TestApplicationRuby): assert self.get()['status'] == 500, 'body each error status' - self.stop() + unit_stop() assert ( self.wait_for_record(r'\[error\].+Failed to run ruby script') @@ -350,3 +351,44 @@ class TestRubyApplication(TestApplicationRuby): assert len(headers['X-Release-Date']) > 0, 'RUBY_RELEASE_DATE' assert len(headers['X-Revision']) > 0, 'RUBY_REVISION' assert len(headers['X-Version']) > 0, 'RUBY_VERSION' + + def test_ruby_application_threads(self): + self.load('threads') + + assert 'success' in self.conf( + '4', 'applications/threads/threads' + ), 'configure 4 threads' + + socks = [] + + for i in range(4): + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '2', + 'Connection': 'close', + }, + no_recv=True, + start=True, + ) + + socks.append(sock) + + threads = set() + + for sock in socks: + resp = self.recvall(sock).decode('utf-8') + + self.log_in(resp) + + resp = self._resp_to_dict(resp) + + assert resp['status'] == 200, 'status' + + threads.add(resp['headers']['X-Thread']) + + assert resp['headers']['Rack-Multithread'] == 'true', 'multithread' + + sock.close() + + assert len(socks) == len(threads), 'threads differs' diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index 13ca0e16..69e25de9 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -1,7 +1,10 @@ +import shutil import pytest from conftest import option +from conftest import unit_run +from conftest import unit_stop from unit.applications.lang.ruby import TestApplicationRuby from unit.feature.isolation import TestFeatureIsolation @@ -9,33 +12,63 @@ from unit.feature.isolation import TestFeatureIsolation class TestRubyIsolation(TestApplicationRuby): prerequisites = {'modules': {'ruby': 'any'}, 'features': ['isolation']} - isolation = TestFeatureIsolation() - @classmethod def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) + check = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.temp_dir) + unit = unit_run() + option.temp_dir = unit['temp_dir'] - return unit if not complete_check else unit.complete() + TestFeatureIsolation().check(option.available, unit['temp_dir']) - def test_ruby_isolation_rootfs(self, is_su): - isolation_features = self.available['features']['isolation'].keys() + assert unit_stop() is None + shutil.rmtree(unit['temp_dir']) - if 'mnt' not in isolation_features: - pytest.skip('requires mnt ns') + return check if not complete_check else check() - if not is_su: - if 'user' not in isolation_features: - pytest.skip('requires unprivileged userns or root') + def test_ruby_isolation_rootfs_mount_namespace(self, is_su): + isolation_features = option.available['features']['isolation'].keys() + if not is_su: if not 'unprivileged_userns_clone' in isolation_features: pytest.skip('requires unprivileged userns or root') - isolation = { - 'namespaces': {'credential': not is_su, 'mount': True}, - 'rootfs': option.test_dir, - } + if 'user' not in isolation_features: + pytest.skip('user namespace is not supported') + + if 'mnt' not in isolation_features: + pytest.skip('mnt namespace is not supported') + + if 'pid' not in isolation_features: + pytest.skip('pid namespace is not supported') + + isolation = {'rootfs': option.test_dir} + + if not is_su: + isolation['namespaces'] = { + 'mount': True, + 'credential': True, + 'pid': True + } + + self.load('status_int', isolation=isolation) + + assert 'success' in self.conf( + '"/ruby/status_int/config.ru"', 'applications/status_int/script', + ) + + assert 'success' in self.conf( + '"/ruby/status_int"', 'applications/status_int/working_directory', + ) + + assert self.get()['status'] == 200, 'status int' + + def test_ruby_isolation_rootfs(self, is_su): + if not is_su: + pytest.skip('requires root') + return + + isolation = {'rootfs': option.test_dir} self.load('status_int', isolation=isolation) diff --git a/test/test_settings.py b/test/test_settings.py index b0af6b04..22830a3b 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -146,14 +146,14 @@ Connection: close assert resp['status'] == 200, 'status body read timeout update' - def test_settings_send_timeout(self): + def test_settings_send_timeout(self, temp_dir): self.load('mirror') data_len = 1048576 self.conf({'http': {'send_timeout': 1}}, 'settings') - addr = self.temp_dir + '/sock' + addr = temp_dir + '/sock' self.conf({"unix:" + addr: {'application': 'mirror'}}, 'listeners') @@ -260,3 +260,41 @@ Connection: close assert 'error' in self.conf( {'http': {'max_body_size': -1}}, 'settings' ), 'settings negative value' + + def test_settings_body_buffer_size(self): + self.load('mirror') + + assert 'success' in self.conf( + { + 'http': { + 'max_body_size': 64 * 1024 * 1024, + 'body_buffer_size': 32 * 1024 * 1024, + } + }, + 'settings', + ) + + body = '0123456789abcdef' + resp = self.post(body=body) + assert bool(resp), 'response from application' + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' + + body = '0123456789abcdef' * 1024 * 1024 + resp = self.post(body=body, read_buffer_size=1024 * 1024) + assert bool(resp), 'response from application 2' + assert resp['status'] == 200, 'status 2' + assert resp['body'] == body, 'body 2' + + body = '0123456789abcdef' * 2 * 1024 * 1024 + resp = self.post(body=body, read_buffer_size=1024 * 1024) + assert bool(resp), 'response from application 3' + assert resp['status'] == 200, 'status 3' + assert resp['body'] == body, 'body 3' + + body = '0123456789abcdef' * 3 * 1024 * 1024 + resp = self.post(body=body, read_buffer_size=1024 * 1024) + assert bool(resp), 'response from application 4' + assert resp['status'] == 200, 'status 4' + assert resp['body'] == body, 'body 4' + diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py index 391d0836..462da9de 100644 --- a/test/test_share_fallback.py +++ b/test/test_share_fallback.py @@ -1,5 +1,6 @@ import os +from conftest import option from conftest import skip_alert from unit.applications.proto import TestApplicationProto @@ -8,14 +9,12 @@ class TestStatic(TestApplicationProto): prerequisites = {} def setup_method(self): - super().setup_method() - - os.makedirs(self.temp_dir + '/assets/dir') - with open(self.temp_dir + '/assets/index.html', 'w') as index: + os.makedirs(option.temp_dir + '/assets/dir') + with open(option.temp_dir + '/assets/index.html', 'w') as index: index.write('0123456789') - os.makedirs(self.temp_dir + '/assets/403') - os.chmod(self.temp_dir + '/assets/403', 0o000) + os.makedirs(option.temp_dir + '/assets/403') + os.chmod(option.temp_dir + '/assets/403', 0o000) self._load_conf( { @@ -23,15 +22,13 @@ class TestStatic(TestApplicationProto): "*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}, }, - "routes": [{"action": {"share": self.temp_dir + "/assets"}}], + "routes": [{"action": {"share": option.temp_dir + "/assets"}}], "applications": {}, } ) def teardown_method(self): - os.chmod(self.temp_dir + '/assets/403', 0o777) - - super().teardown_method() + os.chmod(option.temp_dir + '/assets/403', 0o777) def action_update(self, conf): assert 'success' in self.conf(conf, 'routes/0/action') @@ -46,9 +43,9 @@ class TestStatic(TestApplicationProto): assert resp['status'] == 200, 'bad path fallback status' assert resp['body'] == '', 'bad path fallback' - def test_fallback_valid_path(self): + def test_fallback_valid_path(self, temp_dir): self.action_update( - {"share": self.temp_dir + "/assets", "fallback": {"return": 200}} + {"share": temp_dir + "/assets", "fallback": {"return": 200}} ) resp = self.get() assert resp['status'] == 200, 'fallback status' @@ -79,11 +76,11 @@ class TestStatic(TestApplicationProto): assert resp['status'] == 200, 'fallback nested status' assert resp['body'] == '', 'fallback nested' - def test_fallback_share(self): + def test_fallback_share(self, temp_dir): self.action_update( { "share": "/blah", - "fallback": {"share": self.temp_dir + "/assets"}, + "fallback": {"share": temp_dir + "/assets"}, } ) diff --git a/test/test_static.py b/test/test_static.py index 0b82b4e8..a65928ca 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -3,6 +3,7 @@ import socket import pytest +from conftest import option from conftest import waitforfiles from unit.applications.proto import TestApplicationProto @@ -11,13 +12,13 @@ class TestStatic(TestApplicationProto): prerequisites = {} def setup_method(self): - super().setup_method() - - os.makedirs(self.temp_dir + '/assets/dir') - with open(self.temp_dir + '/assets/index.html', 'w') as index, open( - self.temp_dir + '/assets/README', 'w' - ) as readme, open(self.temp_dir + '/assets/log.log', 'w') as log, open( - self.temp_dir + '/assets/dir/file', 'w' + os.makedirs(option.temp_dir + '/assets/dir') + with open(option.temp_dir + '/assets/index.html', 'w') as index, open( + option.temp_dir + '/assets/README', 'w' + ) as readme, open( + option.temp_dir + '/assets/log.log', 'w' + ) as log, open( + option.temp_dir + '/assets/dir/file', 'w' ) as file: index.write('0123456789') readme.write('readme') @@ -27,7 +28,7 @@ class TestStatic(TestApplicationProto): self._load_conf( { "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": self.temp_dir + "/assets"}}], + "routes": [{"action": {"share": option.temp_dir + "/assets"}}], "settings": { "http": { "static": { @@ -54,9 +55,9 @@ class TestStatic(TestApplicationProto): resp['headers']['Content-Type'] == 'text/html' ), 'index not found 2 Content-Type' - def test_static_large_file(self): + def test_static_large_file(self, temp_dir): file_size = 32 * 1024 * 1024 - with open(self.temp_dir + '/assets/large', 'wb') as f: + with open(temp_dir + '/assets/large', 'wb') as f: f.seek(file_size - 1) f.write(b'\0') @@ -65,14 +66,14 @@ class TestStatic(TestApplicationProto): == file_size ), 'large file' - def test_static_etag(self): + def test_static_etag(self, temp_dir): etag = self.get(url='/')['headers']['ETag'] etag_2 = self.get(url='/README')['headers']['ETag'] assert etag != etag_2, 'different ETag' assert etag == self.get(url='/')['headers']['ETag'], 'same ETag' - with open(self.temp_dir + '/assets/index.html', 'w') as f: + with open(temp_dir + '/assets/index.html', 'w') as f: f.write('blah') assert etag != self.get(url='/')['headers']['ETag'], 'new ETag' @@ -83,22 +84,22 @@ class TestStatic(TestApplicationProto): assert resp['headers']['Location'] == '/dir/', 'redirect Location' assert 'Content-Type' not in resp['headers'], 'redirect Content-Type' - def test_static_space_in_name(self): + def test_static_space_in_name(self, temp_dir): os.rename( - self.temp_dir + '/assets/dir/file', - self.temp_dir + '/assets/dir/fi le', + temp_dir + '/assets/dir/file', + temp_dir + '/assets/dir/fi le', ) - assert waitforfiles(self.temp_dir + '/assets/dir/fi le') + assert waitforfiles(temp_dir + '/assets/dir/fi le') assert self.get(url='/dir/fi le')['body'] == 'blah', 'file name' - os.rename(self.temp_dir + '/assets/dir', self.temp_dir + '/assets/di r') - assert waitforfiles(self.temp_dir + '/assets/di r/fi le') + os.rename(temp_dir + '/assets/dir', temp_dir + '/assets/di r') + assert waitforfiles(temp_dir + '/assets/di r/fi le') assert self.get(url='/di r/fi le')['body'] == 'blah', 'dir name' os.rename( - self.temp_dir + '/assets/di r', self.temp_dir + '/assets/ di r ' + temp_dir + '/assets/di r', temp_dir + '/assets/ di r ' ) - assert waitforfiles(self.temp_dir + '/assets/ di r /fi le') + assert waitforfiles(temp_dir + '/assets/ di r /fi le') assert ( self.get(url='/ di r /fi le')['body'] == 'blah' ), 'dir name enclosing' @@ -121,55 +122,58 @@ class TestStatic(TestApplicationProto): ), 'encoded 2' os.rename( - self.temp_dir + '/assets/ di r /fi le', - self.temp_dir + '/assets/ di r / fi le ', + temp_dir + '/assets/ di r /fi le', + temp_dir + '/assets/ di r / fi le ', ) - assert waitforfiles(self.temp_dir + '/assets/ di r / fi le ') + assert waitforfiles(temp_dir + '/assets/ di r / fi le ') assert ( self.get(url='/%20di%20r%20/%20fi%20le%20')['body'] == 'blah' ), 'file name enclosing' try: - open(self.temp_dir + '/ф а', 'a').close() + open(temp_dir + '/ф а', 'a').close() utf8 = True + except KeyboardInterrupt: + raise + except: utf8 = False if utf8: os.rename( - self.temp_dir + '/assets/ di r / fi le ', - self.temp_dir + '/assets/ di r /фа йл', + temp_dir + '/assets/ di r / fi le ', + temp_dir + '/assets/ di r /фа йл', ) - assert waitforfiles(self.temp_dir + '/assets/ di r /фа йл') + assert waitforfiles(temp_dir + '/assets/ di r /фа йл') assert ( self.get(url='/ di r /фа йл')['body'] == 'blah' ), 'file name 2' os.rename( - self.temp_dir + '/assets/ di r ', - self.temp_dir + '/assets/ди ректория', + temp_dir + '/assets/ di r ', + temp_dir + '/assets/ди ректория', ) - assert waitforfiles(self.temp_dir + '/assets/ди ректория/фа йл') + assert waitforfiles(temp_dir + '/assets/ди ректория/фа йл') assert ( self.get(url='/ди ректория/фа йл')['body'] == 'blah' ), 'dir name 2' - def test_static_unix_socket(self): + def test_static_unix_socket(self, temp_dir): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(self.temp_dir + '/assets/unix_socket') + sock.bind(temp_dir + '/assets/unix_socket') assert self.get(url='/unix_socket')['status'] == 404, 'socket' sock.close() - def test_static_unix_fifo(self): - os.mkfifo(self.temp_dir + '/assets/fifo') + def test_static_unix_fifo(self, temp_dir): + os.mkfifo(temp_dir + '/assets/fifo') assert self.get(url='/fifo')['status'] == 404, 'fifo' - def test_static_symlink(self): - os.symlink(self.temp_dir + '/assets/dir', self.temp_dir + '/assets/link') + def test_static_symlink(self, temp_dir): + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') assert self.get(url='/dir')['status'] == 301, 'dir' assert self.get(url='/dir/file')['status'] == 200, 'file' @@ -312,7 +316,7 @@ class TestStatic(TestApplicationProto): ), 'mime_types same extensions case insensitive' @pytest.mark.skip('not yet') - def test_static_mime_types_invalid(self): + def test_static_mime_types_invalid(self, temp_dir): assert 'error' in self.http( b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r Host: localhost\r @@ -323,5 +327,5 @@ Content-Length: 6\r raw_resp=True, raw=True, sock_type='unix', - addr=self.temp_dir + '/control.unit.sock', + addr=temp_dir + '/control.unit.sock', ), 'mime_types invalid' diff --git a/test/test_tls.py b/test/test_tls.py index 518a834c..4cf8d22c 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -5,6 +5,7 @@ import subprocess import pytest +from conftest import option from conftest import skip_alert from unit.applications.tls import TestApplicationTLS @@ -13,7 +14,7 @@ class TestTLS(TestApplicationTLS): prerequisites = {'modules': {'python': 'any', 'openssl': 'any'}} def findall(self, pattern): - with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: + with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def openssl_date_to_sec_epoch(self, date): @@ -134,7 +135,7 @@ class TestTLS(TestApplicationTLS): self.conf_get('/certificates/default/key') == 'RSA (2048 bits)' ), 'certificate key rsa' - def test_tls_certificate_key_ec(self): + def test_tls_certificate_key_ec(self, temp_dir): self.load('empty') self.openssl_conf() @@ -146,7 +147,7 @@ class TestTLS(TestApplicationTLS): '-noout', '-genkey', '-out', - self.temp_dir + '/ec.key', + temp_dir + '/ec.key', '-name', 'prime256v1', ], @@ -162,11 +163,11 @@ class TestTLS(TestApplicationTLS): '-subj', '/CN=ec/', '-config', - self.temp_dir + '/openssl.conf', + temp_dir + '/openssl.conf', '-key', - self.temp_dir + '/ec.key', + temp_dir + '/ec.key', '-out', - self.temp_dir + '/ec.crt', + temp_dir + '/ec.crt', ], stderr=subprocess.STDOUT, ) @@ -208,7 +209,7 @@ class TestTLS(TestApplicationTLS): == 2592000 ), 'certificate validity until' - def test_tls_certificate_chain(self): + def test_tls_certificate_chain(self, temp_dir): self.load('empty') self.certificate('root', False) @@ -221,11 +222,11 @@ class TestTLS(TestApplicationTLS): '-subj', '/CN=int/', '-config', - self.temp_dir + '/openssl.conf', + temp_dir + '/openssl.conf', '-out', - self.temp_dir + '/int.csr', + temp_dir + '/int.csr', '-keyout', - self.temp_dir + '/int.key', + temp_dir + '/int.key', ], stderr=subprocess.STDOUT, ) @@ -238,16 +239,16 @@ class TestTLS(TestApplicationTLS): '-subj', '/CN=end/', '-config', - self.temp_dir + '/openssl.conf', + temp_dir + '/openssl.conf', '-out', - self.temp_dir + '/end.csr', + temp_dir + '/end.csr', '-keyout', - self.temp_dir + '/end.key', + temp_dir + '/end.key', ], stderr=subprocess.STDOUT, ) - with open(self.temp_dir + '/ca.conf', 'w') as f: + with open(temp_dir + '/ca.conf', 'w') as f: f.write( """[ ca ] default_ca = myca @@ -267,16 +268,16 @@ commonName = supplied [ myca_extensions ] basicConstraints = critical,CA:TRUE""" % { - 'dir': self.temp_dir, - 'database': self.temp_dir + '/certindex', - 'certserial': self.temp_dir + '/certserial', + 'dir': temp_dir, + 'database': temp_dir + '/certindex', + 'certserial': temp_dir + '/certserial', } ) - with open(self.temp_dir + '/certserial', 'w') as f: + with open(temp_dir + '/certserial', 'w') as f: f.write('1000') - with open(self.temp_dir + '/certindex', 'w') as f: + with open(temp_dir + '/certindex', 'w') as f: f.write('') subprocess.call( @@ -287,15 +288,15 @@ basicConstraints = critical,CA:TRUE""" '-subj', '/CN=int/', '-config', - self.temp_dir + '/ca.conf', + temp_dir + '/ca.conf', '-keyfile', - self.temp_dir + '/root.key', + temp_dir + '/root.key', '-cert', - self.temp_dir + '/root.crt', + temp_dir + '/root.crt', '-in', - self.temp_dir + '/int.csr', + temp_dir + '/int.csr', '-out', - self.temp_dir + '/int.crt', + temp_dir + '/int.crt', ], stderr=subprocess.STDOUT, ) @@ -308,22 +309,22 @@ basicConstraints = critical,CA:TRUE""" '-subj', '/CN=end/', '-config', - self.temp_dir + '/ca.conf', + temp_dir + '/ca.conf', '-keyfile', - self.temp_dir + '/int.key', + temp_dir + '/int.key', '-cert', - self.temp_dir + '/int.crt', + temp_dir + '/int.crt', '-in', - self.temp_dir + '/end.csr', + temp_dir + '/end.csr', '-out', - self.temp_dir + '/end.crt', + temp_dir + '/end.crt', ], stderr=subprocess.STDOUT, ) - crt_path = self.temp_dir + '/end-int.crt' - end_path = self.temp_dir + '/end.crt' - int_path = self.temp_dir + '/int.crt' + crt_path = temp_dir + '/end-int.crt' + end_path = temp_dir + '/end.crt' + int_path = temp_dir + '/int.crt' with open(crt_path, 'wb') as crt, open(end_path, 'rb') as end, open( int_path, 'rb' @@ -333,7 +334,7 @@ basicConstraints = critical,CA:TRUE""" self.context = ssl.create_default_context() self.context.check_hostname = False self.context.verify_mode = ssl.CERT_REQUIRED - self.context.load_verify_locations(self.temp_dir + '/root.crt') + self.context.load_verify_locations(temp_dir + '/root.crt') # incomplete chain @@ -485,6 +486,10 @@ basicConstraints = critical,CA:TRUE""" resp = self.get_ssl( headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock ) + + except KeyboardInterrupt: + raise + except: resp = None diff --git a/test/test_upstreams_rr.py b/test/test_upstreams_rr.py index 2ecf1d9a..c20d6054 100644 --- a/test/test_upstreams_rr.py +++ b/test/test_upstreams_rr.py @@ -9,8 +9,6 @@ class TestUpstreamsRR(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} def setup_method(self): - super().setup_method() - assert 'success' in self.conf( { "listeners": { @@ -391,9 +389,9 @@ Connection: close assert sum(resps) == 100, 'post sum' assert abs(resps[0] - resps[1]) <= self.cpu_count, 'post' - def test_upstreams_rr_unix(self): - addr_0 = self.temp_dir + '/sock_0' - addr_1 = self.temp_dir + '/sock_1' + def test_upstreams_rr_unix(self, temp_dir): + addr_0 = temp_dir + '/sock_0' + addr_1 = temp_dir + '/sock_1' assert 'success' in self.conf( { diff --git a/test/test_usr1.py b/test/test_usr1.py index 2e48c18f..3e44e4c5 100644 --- a/test/test_usr1.py +++ b/test/test_usr1.py @@ -1,6 +1,7 @@ import os from subprocess import call +from conftest import unit_stop from conftest import waitforfiles from unit.applications.lang.python import TestApplicationPython @@ -8,12 +9,12 @@ from unit.applications.lang.python import TestApplicationPython class TestUSR1(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def test_usr1_access_log(self): + def test_usr1_access_log(self, temp_dir): self.load('empty') log = 'access.log' log_new = 'new.log' - log_path = self.temp_dir + '/' + log + log_path = temp_dir + '/' + log assert 'success' in self.conf( '"' + log_path + '"', 'access_log' @@ -21,7 +22,7 @@ class TestUSR1(TestApplicationPython): assert waitforfiles(log_path), 'open' - os.rename(log_path, self.temp_dir + '/' + log_new) + os.rename(log_path, temp_dir + '/' + log_new) assert self.get()['status'] == 200 @@ -31,7 +32,7 @@ class TestUSR1(TestApplicationPython): ), 'rename new' assert not os.path.isfile(log_path), 'rename old' - with open(self.temp_dir + '/unit.pid', 'r') as f: + with open(temp_dir + '/unit.pid', 'r') as f: pid = f.read().rstrip() call(['kill', '-s', 'USR1', pid]) @@ -40,7 +41,7 @@ class TestUSR1(TestApplicationPython): assert self.get(url='/usr1')['status'] == 200 - self.stop() + unit_stop() assert ( self.wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log) @@ -48,12 +49,12 @@ class TestUSR1(TestApplicationPython): ), 'reopen 2' assert self.search_in_log(r'/usr1', log_new) is None, 'rename new 2' - def test_usr1_unit_log(self): + def test_usr1_unit_log(self, temp_dir): self.load('log_body') log_new = 'new.log' - log_path = self.temp_dir + '/unit.log' - log_path_new = self.temp_dir + '/' + log_new + log_path = temp_dir + '/unit.log' + log_path_new = temp_dir + '/' + log_new os.rename(log_path, log_path_new) @@ -63,7 +64,7 @@ class TestUSR1(TestApplicationPython): assert self.wait_for_record(body, log_new) is not None, 'rename new' assert not os.path.isfile(log_path), 'rename old' - with open(self.temp_dir + '/unit.pid', 'r') as f: + with open(temp_dir + '/unit.pid', 'r') as f: pid = f.read().rstrip() call(['kill', '-s', 'USR1', pid]) @@ -73,7 +74,7 @@ class TestUSR1(TestApplicationPython): body = 'body_for_a_log_unit' assert self.post(body=body)['status'] == 200 - self.stop() + unit_stop() assert self.wait_for_record(body) is not None, 'rename new' assert self.search_in_log(body, log_new) is None, 'rename new 2' diff --git a/test/test_variables.py b/test/test_variables.py index c458b636..bbb8f769 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -5,8 +5,6 @@ class TestVariables(TestApplicationProto): prerequisites = {} def setup_method(self): - super().setup_method() - assert 'success' in self.conf( { "listeners": {"*:7080": {"pass": "routes/$method"}}, diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 7715bd6c..866dec47 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -7,8 +7,8 @@ from unit.applications.proto import TestApplicationProto class TestApplicationGo(TestApplicationProto): def prepare_env(self, script, name, static=False): - if not os.path.exists(self.temp_dir + '/go'): - os.mkdir(self.temp_dir + '/go') + if not os.path.exists(option.temp_dir + '/go'): + os.mkdir(option.temp_dir + '/go') env = os.environ.copy() env['GOPATH'] = option.current_dir + '/build/go' @@ -22,7 +22,7 @@ class TestApplicationGo(TestApplicationProto): '-ldflags', '-extldflags "-static"', '-o', - self.temp_dir + '/go/' + name, + option.temp_dir + '/go/' + name, option.test_dir + '/go/' + script + '/' + name + '.go', ] else: @@ -30,14 +30,20 @@ class TestApplicationGo(TestApplicationProto): 'go', 'build', '-o', - self.temp_dir + '/go/' + name, + option.temp_dir + '/go/' + name, option.test_dir + '/go/' + script + '/' + name + '.go', ] + if option.detailed: + print("\n$ GOPATH=" + env['GOPATH'] + " " + " ".join(args)) + try: process = subprocess.Popen(args, env=env) process.communicate() + except KeyboardInterrupt: + raise + except: return None @@ -47,7 +53,7 @@ class TestApplicationGo(TestApplicationProto): static_build = False wdir = option.test_dir + "/go/" + script - executable = self.temp_dir + "/go/" + name + executable = option.temp_dir + "/go/" + name if 'isolation' in kwargs and 'rootfs' in kwargs['isolation']: wdir = "/go/" diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index 01cbfa0b..0ff85187 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -9,8 +9,10 @@ from unit.applications.proto import TestApplicationProto class TestApplicationJava(TestApplicationProto): - def load(self, script, name='app', **kwargs): - app_path = self.temp_dir + '/java' + application_type = "java" + + def prepare_env(self, script): + app_path = option.temp_dir + '/java' web_inf_path = app_path + '/WEB-INF/' classes_path = web_inf_path + 'classes/' script_path = option.test_dir + '/java/' + script + '/' @@ -50,7 +52,7 @@ class TestApplicationJava(TestApplicationProto): os.makedirs(classes_path) classpath = ( - option.current_dir + '/build/tomcat-servlet-api-9.0.13.jar' + option.current_dir + '/build/tomcat-servlet-api-9.0.39.jar' ) ws_jars = glob.glob( @@ -62,18 +64,28 @@ class TestApplicationJava(TestApplicationProto): javac = [ 'javac', + '-target', '8', '-source', '8', '-nowarn', '-encoding', 'utf-8', '-d', classes_path, '-classpath', classpath + ':' + ws_jars[0], ] javac.extend(src) + if option.detailed: + print("\n$ " + " ".join(javac)) + try: process = subprocess.Popen(javac, stderr=subprocess.STDOUT) process.communicate() + except KeyboardInterrupt: + raise + except: - pytest.fail('Cann\'t run javac process.') + pytest.fail('Can\'t run javac process.') + + def load(self, script, **kwargs): + self.prepare_env(script) self._load_conf( { @@ -81,10 +93,13 @@ class TestApplicationJava(TestApplicationProto): "applications": { script: { "unit_jars": option.current_dir + '/build', - "type": 'java', + "type": self.get_application_type(), "processes": {"spare": 0}, - "working_directory": script_path, - "webapp": app_path, + "working_directory": option.test_dir + + '/java/' + + script + + '/', + "webapp": option.temp_dir + '/java', } }, }, diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index 877fc461..98fd9ffc 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -7,21 +7,24 @@ from unit.applications.proto import TestApplicationProto class TestApplicationNode(TestApplicationProto): - def load(self, script, name='app.js', **kwargs): + def prepare_env(self, script): # copy application shutil.copytree( - option.test_dir + '/node/' + script, self.temp_dir + '/node' + option.test_dir + '/node/' + script, option.temp_dir + '/node' ) # copy modules shutil.copytree( option.current_dir + '/node/node_modules', - self.temp_dir + '/node/node_modules', + option.temp_dir + '/node/node_modules', ) - public_dir(self.temp_dir + '/node') + public_dir(option.temp_dir + '/node') + + def load(self, script, name='app.js', **kwargs): + self.prepare_env(script) self._load_conf( { @@ -32,7 +35,7 @@ class TestApplicationNode(TestApplicationProto): script: { "type": "external", "processes": {"spare": 0}, - "working_directory": self.temp_dir + '/node', + "working_directory": option.temp_dir + '/node', "executable": name, } }, diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py index a27c7649..9dc24ace 100644 --- a/test/unit/applications/lang/perl.py +++ b/test/unit/applications/lang/perl.py @@ -7,17 +7,13 @@ class TestApplicationPerl(TestApplicationProto): def load(self, script, name='psgi.pl', **kwargs): script_path = option.test_dir + '/perl/' + script - appication_type = self.get_appication_type() - - if appication_type is None: - appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": appication_type, + "type": self.get_application_type(), "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 index 2d50df2e..3dbb32f5 100644 --- a/test/unit/applications/lang/php.py +++ b/test/unit/applications/lang/php.py @@ -1,4 +1,7 @@ from conftest import option +import os +import shutil + from unit.applications.proto import TestApplicationProto @@ -7,17 +10,24 @@ class TestApplicationPHP(TestApplicationProto): def load(self, script, index='index.php', **kwargs): script_path = option.test_dir + '/php/' + script - appication_type = self.get_appication_type() - if appication_type is None: - appication_type = self.application_type + if kwargs.get('isolation') and kwargs['isolation'].get('rootfs'): + rootfs = kwargs['isolation']['rootfs'] + + if not os.path.exists(rootfs + '/app/php/'): + os.makedirs(rootfs + '/app/php/') + + if not os.path.exists(rootfs + '/app/php/' + script): + shutil.copytree(script_path, rootfs + '/app/php/' + script) + + script_path = '/app/php/' + script self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": appication_type, + "type": self.get_application_type(), "processes": {"spare": 0}, "root": script_path, "working_directory": script_path, diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 47b95dac..792a86fa 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -12,7 +12,6 @@ class TestApplicationPython(TestApplicationProto): load_module = "wsgi" def load(self, script, name=None, module=None, **kwargs): - print() if name is None: name = script @@ -35,25 +34,25 @@ class TestApplicationPython(TestApplicationProto): script_path = '/app/python/' + name - appication_type = self.get_appication_type() + app = { + "type": self.get_application_type(), + "processes": kwargs.pop('processes', {"spare": 0}), + "path": script_path, + "working_directory": script_path, + "module": module, + } - if appication_type is None: - appication_type = self.application_type + for attr in ('callable', 'home', 'limits', 'path', 'protocol', + 'threads'): + if attr in kwargs: + app[attr] = kwargs.pop(attr) self._load_conf( { "listeners": { "*:7080": {"pass": "applications/" + quote(name, '')} }, - "applications": { - name: { - "type": appication_type, - "processes": {"spare": 0}, - "path": script_path, - "working_directory": script_path, - "module": module, - } - }, + "applications": {name: app}, }, **kwargs ) diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index bc3cefc6..82d66e65 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -7,17 +7,13 @@ class TestApplicationRuby(TestApplicationProto): def load(self, script, name='config.ru', **kwargs): script_path = option.test_dir + '/ruby/' + script - appication_type = self.get_appication_type() - - if appication_type is None: - appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": appication_type, + "type": self.get_application_type(), "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 index 2f748c21..6e760c70 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -7,6 +7,8 @@ from unit.control import TestControl class TestApplicationProto(TestControl): + application_type = None + def sec_epoch(self): return time.mktime(time.gmtime()) @@ -14,7 +16,7 @@ class TestApplicationProto(TestControl): return time.mktime(time.strptime(date, template)) def search_in_log(self, pattern, name='unit.log'): - with open(self.temp_dir + '/' + name, 'r', errors='ignore') as f: + with open(option.temp_dir + '/' + name, 'r', errors='ignore') as f: return re.search(pattern, f.read()) def wait_for_record(self, pattern, name='unit.log'): @@ -28,15 +30,12 @@ class TestApplicationProto(TestControl): return found - def get_appication_type(self): + def get_application_type(self): current_test = ( os.environ.get('PYTEST_CURRENT_TEST').split(':')[-1].split(' ')[0] ) - if current_test in option.generated_tests: - return option.generated_tests[current_test] - - return None + return option.generated_tests.get(current_test, self.application_type) def _load_conf(self, conf, **kwargs): if 'applications' in conf: diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index fdf681ae..fb1b112c 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -8,8 +8,6 @@ from unit.applications.proto import TestApplicationProto class TestApplicationTLS(TestApplicationProto): def setup_method(self): - super().setup_method() - self.context = ssl.create_default_context() self.context.check_hostname = False self.context.verify_mode = ssl.CERT_NONE @@ -24,9 +22,9 @@ class TestApplicationTLS(TestApplicationProto): '-x509', '-new', '-subj', '/CN=' + name + '/', - '-config', self.temp_dir + '/openssl.conf', - '-out', self.temp_dir + '/' + name + '.crt', - '-keyout', self.temp_dir + '/' + name + '.key', + '-config', option.temp_dir + '/openssl.conf', + '-out', option.temp_dir + '/' + name + '.crt', + '-keyout', option.temp_dir + '/' + name + '.key', ], stderr=subprocess.STDOUT, ) @@ -38,8 +36,8 @@ class TestApplicationTLS(TestApplicationProto): if key is None: key = crt - key_path = self.temp_dir + '/' + key + '.key' - crt_path = self.temp_dir + '/' + crt + '.crt' + key_path = option.temp_dir + '/' + key + '.key' + crt_path = option.temp_dir + '/' + crt + '.crt' with open(key_path, 'rb') as k, open(crt_path, 'rb') as c: return self.conf(k.read() + c.read(), '/certificates/' + crt) @@ -66,7 +64,7 @@ class TestApplicationTLS(TestApplicationProto): return ssl.get_server_certificate(addr, ssl_version=ssl_version) def openssl_conf(self): - conf_path = self.temp_dir + '/openssl.conf' + conf_path = option.temp_dir + '/openssl.conf' if os.path.exists(conf_path): return diff --git a/test/unit/check/go.py b/test/unit/check/go.py index dd2150eb..35b0c2d5 100644 --- a/test/unit/check/go.py +++ b/test/unit/check/go.py @@ -25,5 +25,8 @@ def check_go(current_dir, temp_dir, test_dir): if process.returncode == 0: return True + except KeyboardInterrupt: + raise + except: return None diff --git a/test/unit/control.py b/test/unit/control.py index 6fd350f4..f05aa827 100644 --- a/test/unit/control.py +++ b/test/unit/control.py @@ -1,5 +1,6 @@ import json +from conftest import option from unit.http import TestHTTP @@ -53,7 +54,7 @@ class TestControl(TestHTTP): args = { 'url': url, 'sock_type': 'unix', - 'addr': self.temp_dir + '/control.unit.sock', + 'addr': option.temp_dir + '/control.unit.sock', } if conf is not None: diff --git a/test/unit/feature/isolation.py b/test/unit/feature/isolation.py index c6f6f3c0..7877c03a 100644 --- a/test/unit/feature/isolation.py +++ b/test/unit/feature/isolation.py @@ -3,11 +3,8 @@ import os from unit.applications.lang.go import TestApplicationGo from unit.applications.lang.java import TestApplicationJava from unit.applications.lang.node import TestApplicationNode -from unit.applications.lang.perl import TestApplicationPerl -from unit.applications.lang.php import TestApplicationPHP -from unit.applications.lang.python import TestApplicationPython -from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.proto import TestApplicationProto +from conftest import option class TestFeatureIsolation(TestApplicationProto): @@ -16,40 +13,119 @@ class TestFeatureIsolation(TestApplicationProto): def check(self, available, temp_dir): test_conf = {"namespaces": {"credential": True}} - module = '' - app = 'empty' + conf = '' if 'go' in available['modules']: - module = TestApplicationGo() + TestApplicationGo().prepare_env('empty', 'app') + + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "external", + "processes": {"spare": 0}, + "working_directory": option.test_dir + "/go/empty", + "executable": option.temp_dir + "/go/app", + "isolation": {"namespaces": {"credential": True}}, + }, + }, + } - elif 'java' in available['modules']: - module = TestApplicationJava() - - elif 'node' in available['modules']: - module = TestApplicationNode() - app = 'basic' - - elif 'perl' in available['modules']: - module = TestApplicationPerl() - app = 'body_empty' + elif 'python' in available['modules']: + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/empty", + "working_directory": option.test_dir + "/python/empty", + "module": "wsgi", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } elif 'php' in available['modules']: - module = TestApplicationPHP() - app = 'phpinfo' - - elif 'python' in available['modules']: - module = TestApplicationPython() + conf = { + "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, + "applications": { + "phpinfo": { + "type": "php", + "processes": {"spare": 0}, + "root": option.test_dir + "/php/phpinfo", + "working_directory": option.test_dir + "/php/phpinfo", + "index": "index.php", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } elif 'ruby' in available['modules']: - module = TestApplicationRuby() + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "ruby", + "processes": {"spare": 0}, + "working_directory": option.test_dir + "/ruby/empty", + "script": option.test_dir + "/ruby/empty/config.ru", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } - if not module: - return + elif 'java' in available['modules']: + TestApplicationJava().prepare_env('empty') + + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "unit_jars": option.current_dir + "/build", + "type": "java", + "processes": {"spare": 0}, + "working_directory": option.test_dir + "/java/empty/", + "webapp": option.temp_dir + "/java", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } - module.temp_dir = temp_dir - module.load(app) + elif 'node' in available['modules']: + TestApplicationNode().prepare_env('basic') + + conf = { + "listeners": {"*:7080": {"pass": "applications/basic"}}, + "applications": { + "basic": { + "type": "external", + "processes": {"spare": 0}, + "working_directory": option.temp_dir + "/node", + "executable": "app.js", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + elif 'perl' in available['modules']: + conf = { + "listeners": {"*:7080": {"pass": "applications/body_empty"}}, + "applications": { + "body_empty": { + "type": "perl", + "processes": {"spare": 0}, + "working_directory": option.test_dir + + "/perl/body_empty", + "script": option.test_dir + "/perl/body_empty/psgi.pl", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + else: + return - resp = module.conf(test_conf, 'applications/' + app + '/isolation') - if 'success' not in resp: + if 'success' not in self.conf(conf): return userns = self.getns('user') diff --git a/test/unit/http.py b/test/unit/http.py index 7845f9a8..8d964978 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -5,7 +5,6 @@ import os import re import select import socket -import time import pytest from conftest import option @@ -188,6 +187,10 @@ class TestHTTP(TestUnit): try: part = sock.recv(buff_size) + + except KeyboardInterrupt: + raise + except: break @@ -243,7 +246,8 @@ class TestHTTP(TestUnit): try: last_size = int(chunks[-2], 16) - except: + + except ValueError: pytest.fail('Invalid zero size chunk') if last_size != 0 or chunks[-1] != b'': @@ -253,7 +257,8 @@ class TestHTTP(TestUnit): while len(chunks) >= 2: try: size = int(chunks.pop(0), 16) - except: + + except ValueError: pytest.fail('Invalid chunk size %s' % str(size)) if size == 0: @@ -283,23 +288,6 @@ class TestHTTP(TestUnit): def getjson(self, **kwargs): return self.get(json=True, **kwargs) - def waitforsocket(self, port): - ret = False - - for i in range(50): - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(('127.0.0.1', port)) - ret = True - break - except: - sock.close() - time.sleep(0.1) - - sock.close() - - assert ret, 'socket connected' - def form_encode(self, fields): is_multipart = False diff --git a/test/unit/main.py b/test/unit/main.py index d5940995..488b3f4d 100644 --- a/test/unit/main.py +++ b/test/unit/main.py @@ -1,55 +1,19 @@ -import atexit -import os -import re -import shutil -import signal -import stat -import subprocess -import tempfile -import time -from multiprocessing import Process - import pytest -from conftest import _check_alerts -from conftest import _print_log from conftest import option -from conftest import public_dir -from conftest import waitforfiles class TestUnit(): @classmethod def setup_class(cls, complete_check=True): - cls.available = option.available - unit = TestUnit() - - unit._run() - - # read unit.log - - for i in range(50): - with open(unit.temp_dir + '/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: - _print_log(path=unit.temp_dir + '/unit.log') - exit("Unit is writing log too long") - - def check(available, prerequisites): + def check(): missed = [] # check modules - if 'modules' in prerequisites: - available_modules = list(available['modules'].keys()) + if 'modules' in cls.prerequisites: + available_modules = list(option.available['modules'].keys()) - for module in prerequisites['modules']: + for module in cls.prerequisites['modules']: if module in available_modules: continue @@ -60,10 +24,10 @@ class TestUnit(): # check features - if 'features' in prerequisites: - available_features = list(available['features'].keys()) + if 'features' in cls.prerequisites: + available_features = list(option.available['features'].keys()) - for feature in prerequisites['features']: + for feature in cls.prerequisites['features']: if feature in available_features: continue @@ -72,132 +36,7 @@ class TestUnit(): if missed: pytest.skip(', '.join(missed) + ' feature(s) not supported') - def destroy(): - unit.stop() - _check_alerts(log) - shutil.rmtree(unit.temp_dir) - - def complete(): - destroy() - check(cls.available, cls.prerequisites) - if complete_check: - complete() - else: - unit.complete = complete - return unit - - def setup_method(self): - self._run() - - def _run(self): - build_dir = option.current_dir + '/build' - self.unitd = build_dir + '/unitd' - - if not os.path.isfile(self.unitd): - exit("Could not find unit") - - self.temp_dir = tempfile.mkdtemp(prefix='unit-test-') - - public_dir(self.temp_dir) - - if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': - public_dir(build_dir) - - os.mkdir(self.temp_dir + '/state') - - with open(self.temp_dir + '/unit.log', 'w') as log: - self._p = subprocess.Popen( - [ - self.unitd, - '--no-daemon', - '--modules', build_dir, - '--state', self.temp_dir + '/state', - '--pid', self.temp_dir + '/unit.pid', - '--log', self.temp_dir + '/unit.log', - '--control', 'unix:' + self.temp_dir + '/control.unit.sock', - '--tmp', self.temp_dir, - ], - stderr=log, - ) - - atexit.register(self.stop) - - if not waitforfiles(self.temp_dir + '/control.unit.sock'): - _print_log(path=self.temp_dir + '/unit.log') - exit("Could not start unit") - - self._started = True - - def teardown_method(self): - self.stop() - - # check unit.log for alerts - - unit_log = self.temp_dir + '/unit.log' - - with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: - _check_alerts(f.read()) - - # remove unit.log - - if not option.save_log: - shutil.rmtree(self.temp_dir) + check() else: - _print_log(path=self.temp_dir) - - assert self.stop_errors == [None, None], 'stop errors' - - def stop(self): - if not self._started: - return - - self.stop_errors = [] - - self.stop_errors.append(self._stop()) - - self.stop_errors.append(self.stop_processes()) - - atexit.unregister(self.stop) - - self._started = False - - def _stop(self): - if self._p.poll() is not None: - return - - with self._p as p: - p.send_signal(signal.SIGQUIT) - - try: - retcode = p.wait(15) - if retcode: - return 'Child process terminated with code ' + str(retcode) - except: - p.kill() - return 'Could not terminate unit' - - def run_process(self, target, *args): - if not hasattr(self, '_processes'): - self._processes = [] - - process = Process(target=target, args=args) - process.start() - - self._processes.append(process) - - def stop_processes(self): - if not hasattr(self, '_processes'): - return - - fail = False - for process in self._processes: - if process.is_alive(): - process.terminate() - process.join(timeout=15) - - if process.is_alive(): - fail = True - - if fail: - return 'Fail to stop process' + return check |