diff options
Diffstat (limited to 'test/unit')
-rw-r--r-- | test/unit/applications/lang/go.py | 32 | ||||
-rw-r--r-- | test/unit/applications/lang/java.py | 26 | ||||
-rw-r--r-- | test/unit/applications/lang/node.py | 12 | ||||
-rw-r--r-- | test/unit/applications/tls.py | 22 | ||||
-rw-r--r-- | test/unit/applications/websockets.py | 46 | ||||
-rw-r--r-- | test/unit/feature/isolation.py | 87 | ||||
-rw-r--r-- | test/unit/http.py | 12 | ||||
-rw-r--r-- | test/unit/main.py | 178 |
8 files changed, 283 insertions, 132 deletions
diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index e4ab8ffa..15ac1cd9 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -4,12 +4,22 @@ from unit.applications.proto import TestApplicationProto class TestApplicationGo(TestApplicationProto): - def load(self, script, name='app'): + @classmethod + def setUpClass(cls, complete_check=True): + unit = super().setUpClass(complete_check=False) - if not os.path.isdir(self.testdir + '/go'): - os.mkdir(self.testdir + '/go') + # check go module + + go_app = TestApplicationGo() + go_app.testdir = unit.testdir + if go_app.prepare_env('empty', 'app').returncode == 0: + cls.available['modules']['go'] = [] - go_app_path = self.current_dir + '/go/' + return unit if not complete_check else unit.complete() + + def prepare_env(self, script, name): + if not os.path.exists(self.testdir + '/go'): + os.mkdir(self.testdir + '/go') env = os.environ.copy() env['GOPATH'] = self.pardir + '/go' @@ -19,12 +29,18 @@ class TestApplicationGo(TestApplicationProto): 'build', '-o', self.testdir + '/go/' + name, - go_app_path + script + '/' + name + '.go', + self.current_dir + '/go/' + script + '/' + name + '.go', ], env=env, ) + process.communicate() + return process + + def load(self, script, name='app'): + self.prepare_env(script, name) + self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, @@ -32,8 +48,10 @@ class TestApplicationGo(TestApplicationProto): script: { "type": "external", "processes": {"spare": 0}, - "working_directory": go_app_path + script, - "executable": self.testdir + '/go/' + name, + "working_directory": self.current_dir + + "/go/" + + script, + "executable": self.testdir + "/go/" + name, } }, } diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index ec1c95d9..40bf3662 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -1,4 +1,5 @@ import os +import glob import shutil from subprocess import Popen from unit.applications.proto import TestApplicationProto @@ -6,11 +7,9 @@ from unit.applications.proto import TestApplicationProto class TestApplicationJava(TestApplicationProto): def load(self, script, name='app'): - app_path = self.testdir + '/java' web_inf_path = app_path + '/WEB-INF/' classes_path = web_inf_path + 'classes/' - script_path = self.current_dir + '/java/' + script + '/' if not os.path.isdir(app_path): @@ -19,39 +18,48 @@ class TestApplicationJava(TestApplicationProto): src = [] for f in os.listdir(script_path): + file_path = script_path + f + if f.endswith('.java'): - src.append(script_path + f) + src.append(file_path) continue if f.startswith('.') or f == 'Makefile': continue - if os.path.isdir(script_path + f): + if os.path.isdir(file_path): if f == 'WEB-INF': continue - shutil.copytree(script_path + f, app_path + '/' + f) + shutil.copytree(file_path, app_path + '/' + f) continue if f == 'web.xml': if not os.path.isdir(web_inf_path): os.makedirs(web_inf_path) - shutil.copy2(script_path + f, web_inf_path) + shutil.copy2(file_path, web_inf_path) else: - shutil.copy2(script_path + f, app_path) + shutil.copy2(file_path, app_path) if src: if not os.path.isdir(classes_path): os.makedirs(classes_path) - tomcat_jar = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar' + classpath = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar' + + ws_jars = glob.glob( + self.pardir + '/build/websocket-api-java-*.jar' + ) + + if not ws_jars: + self.fail('websocket api jar not found.') javac = [ 'javac', '-encoding', 'utf-8', '-d', classes_path, - '-classpath', tomcat_jar, + '-classpath', classpath + ':' + ws_jars[0], ] javac.extend(src) diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index 931c6596..3cc72669 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -4,8 +4,18 @@ from unit.applications.proto import TestApplicationProto class TestApplicationNode(TestApplicationProto): - def load(self, script, name='app.js'): + @classmethod + def setUpClass(cls, complete_check=True): + unit = super().setUpClass(complete_check=False) + + # check node module + + if os.path.exists(unit.pardir + '/node/node_modules'): + cls.available['modules']['node'] = [] + return unit if not complete_check else unit.complete() + + def load(self, script, name='app.js'): # copy application shutil.copytree( diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index 6e8deefb..1290279d 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -1,4 +1,5 @@ import os +import re import ssl import subprocess from unit.applications.proto import TestApplicationProto @@ -12,6 +13,27 @@ class TestApplicationTLS(TestApplicationProto): self.context.check_hostname = False self.context.verify_mode = ssl.CERT_NONE + @classmethod + def setUpClass(cls, complete_check=True): + unit = super().setUpClass(complete_check=False) + + # check tls module + + try: + subprocess.check_output(['which', 'openssl']) + + output = subprocess.check_output( + [unit.unitd, '--version'], stderr=subprocess.STDOUT + ) + + if re.search('--openssl', output.decode()): + cls.available['modules']['openssl'] = [] + + except: + pass + + return unit if not complete_check else unit.complete() + def certificate(self, name='default', load=True): self.openssl_conf() diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index 417e9504..50ff2797 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -54,24 +54,16 @@ class TestApplicationWebsocket(TestApplicationProto): def apply_mask(self, data, mask): return bytes(b ^ m for b, m in zip(data, itertools.cycle(mask))) - def serialize_close(self, code = 1000, reason = ''): + def serialize_close(self, code=1000, reason=''): return struct.pack('!H', code) + reason.encode('utf-8') - def frame_read(self, sock, read_timeout=10): + def frame_read(self, sock, read_timeout=30): def recv_bytes(sock, bytes): data = b'' while select.select([sock], [], [], read_timeout)[0]: - try: - if bytes < 65536: - data = sock.recv(bytes) - else: - data = self.recvall( - sock, - read_timeout=read_timeout, - buff_size=bytes, - ) - break - except: + data += sock.recv(bytes - len(data)) + + if len(data) == bytes: break return data @@ -99,7 +91,11 @@ class TestApplicationWebsocket(TestApplicationProto): if frame['mask']: mask_bits = recv_bytes(sock, 4) - data = recv_bytes(sock, length) + data = b'' + + if length != 0: + data = recv_bytes(sock, length) + if frame['mask']: data = self.apply_mask(data, mask_bits) @@ -175,14 +171,20 @@ class TestApplicationWebsocket(TestApplicationProto): frame = self.frame_to_send(*args, **kwargs) if chopsize is None: - sock.sendall(frame) + try: + sock.sendall(frame) + except BrokenPipeError: + pass else: pos = 0 frame_len = len(frame) - while (pos < frame_len): + while pos < frame_len: end = min(pos + chopsize, frame_len) - sock.sendall(frame[pos:end]) + try: + sock.sendall(frame[pos:end]) + except BrokenPipeError: + end = frame_len pos = end def message(self, sock, type, message, fragmention_size=None, **kwargs): @@ -197,17 +199,19 @@ class TestApplicationWebsocket(TestApplicationProto): pos = 0 op_code = type - while(pos < message_len): + while pos < message_len: end = min(pos + fragmention_size, message_len) - fin = (end == message_len) - self.frame_write(sock, op_code, message[pos:end], fin=fin, **kwargs) + fin = end == message_len + self.frame_write( + sock, op_code, message[pos:end], fin=fin, **kwargs + ) op_code = self.OP_CONT pos = end def message_read(self, sock, read_timeout=10): frame = self.frame_read(sock, read_timeout=read_timeout) - while(not frame['fin']): + while not frame['fin']: temp = self.frame_read(sock, read_timeout=read_timeout) frame['data'] += temp['data'] frame['fin'] = temp['fin'] diff --git a/test/unit/feature/isolation.py b/test/unit/feature/isolation.py new file mode 100644 index 00000000..9b06ab3c --- /dev/null +++ b/test/unit/feature/isolation.py @@ -0,0 +1,87 @@ +import os +import json +from unit.applications.proto import TestApplicationProto +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 + + +class TestFeatureIsolation(TestApplicationProto): + allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] + + def check(self, available, testdir): + test_conf = {"namespaces": {"credential": True}} + + module = '' + app = 'empty' + if 'go' in available['modules']: + module = TestApplicationGo() + + 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 'php' in available['modules']: + module = TestApplicationPHP() + app = 'phpinfo' + + elif 'python' in available['modules']: + module = TestApplicationPython() + + elif 'ruby' in available['modules']: + module = TestApplicationRuby() + + if not module: + return + + module.testdir = testdir + module.load(app) + + resp = module.conf(test_conf, 'applications/' + app + '/isolation') + if 'success' not in resp: + return + + userns = self.getns('user') + if not userns: + return + + available['features']['isolation'] = {'user': userns} + + unp_clone_path = '/proc/sys/kernel/unprivileged_userns_clone' + if os.path.exists(unp_clone_path): + with open(unp_clone_path, 'r') as f: + if str(f.read()).rstrip() == '1': + available['features']['isolation'][ + 'unprivileged_userns_clone' + ] = True + + for ns in self.allns: + ns_value = self.getns(ns) + if ns_value: + available['features']['isolation'][ns] = ns_value + + def getns(self, nstype): + # read namespace id from symlink file: + # it points to: '<nstype>:[<ns id>]' + # # eg.: 'pid:[4026531836]' + nspath = '/proc/self/ns/' + nstype + data = None + + if os.path.exists(nspath): + data = int(os.readlink(nspath)[len(nstype) + 2 : -1]) + + return data + + def parsejson(self, data): + return json.loads(data.split('\n')[1]) diff --git a/test/unit/http.py b/test/unit/http.py index c0af8a9e..82a6bd6a 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -12,6 +12,11 @@ class TestHTTP(TestUnit): port = 7080 if 'port' not in kwargs else kwargs['port'] url = '/' if 'url' not in kwargs else kwargs['url'] http = 'HTTP/1.0' if 'http_10' in kwargs else 'HTTP/1.1' + read_buffer_size = ( + 4096 + if 'read_buffer_size' not in kwargs + else kwargs['read_buffer_size'] + ) headers = ( {'Host': 'localhost', 'Connection': 'close'} @@ -94,7 +99,9 @@ class TestHTTP(TestUnit): read_timeout = ( 30 if 'read_timeout' not in kwargs else kwargs['read_timeout'] ) - resp = self.recvall(sock, read_timeout=read_timeout).decode(enc) + resp = self.recvall( + sock, read_timeout=read_timeout, buff_size=read_buffer_size + ).decode(enc) if TestUnit.detailed: print('<<<') @@ -118,6 +125,9 @@ class TestHTTP(TestUnit): def get(self, **kwargs): return self.http('GET', **kwargs) + def head(self, **kwargs): + return self.http('HEAD', **kwargs) + def post(self, **kwargs): return self.http('POST', **kwargs) diff --git a/test/unit/main.py b/test/unit/main.py index 6a167a9e..873f1815 100644 --- a/test/unit/main.py +++ b/test/unit/main.py @@ -12,8 +12,6 @@ import subprocess from multiprocessing import Process -available_modules = {} - class TestUnit(unittest.TestCase): current_dir = os.path.abspath( @@ -28,6 +26,7 @@ class TestUnit(unittest.TestCase): detailed = False save_log = False + unsafe = False def __init__(self, methodName='runTest'): super().__init__(methodName) @@ -41,10 +40,12 @@ class TestUnit(unittest.TestCase): if not hasattr(self, 'application_type'): return super().run(result) + # rerun test for each available module version + type = self.application_type - for prerequisite in self.prerequisites: - if prerequisite in available_modules: - for version in available_modules[prerequisite]: + for module in self.prerequisites['modules']: + if module in self.available['modules']: + for version in self.available['modules'][module]: self.application_type = type + ' ' + version super().run(result) @@ -63,8 +64,83 @@ class TestUnit(unittest.TestCase): unittest.main() @classmethod - def setUpClass(cls): - TestUnit().check_modules(*cls.prerequisites) + def setUpClass(cls, complete_check=True): + cls.available = {'modules': {}, 'features': {}} + unit = TestUnit() + + unit._run() + + # read unit.log + + for i in range(50): + with open(unit.testdir + '/unit.log', 'r') as f: + log = f.read() + m = re.search('controller started', log) + + if m is None: + time.sleep(0.1) + else: + break + + if m is None: + unit.stop() + 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 cls.available['modules']: + cls.available['modules'][module[0]] = [module[1]] + else: + cls.available['modules'][module[0]].append(module[1]) + + def check(available, prerequisites): + missed = [] + + # check modules + + if 'modules' in prerequisites: + available_modules = list(available['modules'].keys()) + + for module in prerequisites['modules']: + if module in available_modules: + continue + + missed.append(module) + + if missed: + print('Unit has no ' + ', '.join(missed) + ' module(s)') + raise unittest.SkipTest() + + # check features + + if 'features' in prerequisites: + available_features = list(available['features'].keys()) + + for feature in prerequisites['features']: + if feature in available_features: + continue + + missed.append(feature) + + if missed: + print(', '.join(missed) + ' feature(s) not supported') + raise unittest.SkipTest() + + def destroy(): + unit.stop() + unit._check_alerts(log) + shutil.rmtree(unit.testdir) + + def complete(): + destroy() + check(cls.available, cls.prerequisites) + + if complete_check: + complete() + else: + unit.complete = complete + return unit def setUp(self): self._run() @@ -105,92 +181,6 @@ class TestUnit(unittest.TestCase): else: self._print_path_to_log() - def check_modules(self, *modules): - self._run() - - for i in range(50): - with open(self.testdir + '/unit.log', 'r') as f: - log = f.read() - m = re.search('controller started', log) - - if m is None: - time.sleep(0.1) - else: - break - - if m is None: - self.stop() - exit("Unit is writing log too long") - - # discover all available modules - - global available_modules - available_modules = {} - for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): - if module[0] not in available_modules: - available_modules[module[0]] = [module[1]] - else: - available_modules[module[0]].append(module[1]) - - missed_module = '' - for module in modules: - if module == 'go': - env = os.environ.copy() - env['GOPATH'] = self.pardir + '/go' - - try: - process = subprocess.Popen( - [ - 'go', - 'build', - '-o', - self.testdir + '/go/check_module', - self.current_dir + '/go/empty/app.go', - ], - env=env, - ) - process.communicate() - - m = module if process.returncode == 0 else None - - except: - m = None - - elif module == 'node': - if os.path.isdir(self.pardir + '/node/node_modules'): - m = module - else: - m = None - - elif module == 'openssl': - try: - subprocess.check_output(['which', 'openssl']) - - output = subprocess.check_output( - [self.unitd, '--version'], - stderr=subprocess.STDOUT, - ) - - m = re.search('--openssl', output.decode()) - - except: - m = None - - else: - if module not in available_modules: - m = None - - if m is None: - missed_module = module - break - - self.stop() - self._check_alerts(log) - shutil.rmtree(self.testdir) - - if missed_module: - raise unittest.SkipTest('Unit has no ' + missed_module + ' module') - def stop(self): if self._started: self._stop() @@ -350,6 +340,8 @@ class TestUnit(unittest.TestCase): TestUnit.save_log = args.save_log TestUnit.unsafe = args.unsafe + # set stdout to non-blocking + if TestUnit.detailed: fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) |