xref: /unit/test/conftest.py (revision 1805:55de51d00bca)
1import fcntl
2import inspect
3import json
4import os
5import platform
6import re
7import shutil
8import signal
9import socket
10import stat
11import subprocess
12import sys
13import tempfile
14import time
15from multiprocessing import Process
16
17import pytest
18from unit.check.go import check_go
19from unit.check.isolation import check_isolation
20from unit.check.node import check_node
21from unit.check.tls import check_openssl
22from unit.http import TestHTTP
23from unit.option import option
24from unit.utils import public_dir
25from unit.utils import waitforfiles
26
27
28def pytest_addoption(parser):
29    parser.addoption(
30        "--detailed",
31        default=False,
32        action="store_true",
33        help="Detailed output for tests",
34    )
35    parser.addoption(
36        "--print-log",
37        default=False,
38        action="store_true",
39        help="Print unit.log to stdout in case of errors",
40    )
41    parser.addoption(
42        "--save-log",
43        default=False,
44        action="store_true",
45        help="Save unit.log after the test execution",
46    )
47    parser.addoption(
48        "--unsafe",
49        default=False,
50        action="store_true",
51        help="Run unsafe tests",
52    )
53    parser.addoption(
54        "--user",
55        type=str,
56        help="Default user for non-privileged processes of unitd",
57    )
58    parser.addoption(
59        "--restart",
60        default=False,
61        action="store_true",
62        help="Force Unit to restart after every test",
63    )
64
65
66unit_instance = {}
67unit_log_copy = "unit.log.copy"
68_processes = []
69http = TestHTTP()
70
71def pytest_configure(config):
72    option.config = config.option
73
74    option.detailed = config.option.detailed
75    option.print_log = config.option.print_log
76    option.save_log = config.option.save_log
77    option.unsafe = config.option.unsafe
78    option.user = config.option.user
79    option.restart = config.option.restart
80
81    option.generated_tests = {}
82    option.current_dir = os.path.abspath(
83        os.path.join(os.path.dirname(__file__), os.pardir)
84    )
85    option.test_dir = option.current_dir + '/test'
86    option.architecture = platform.architecture()[0]
87    option.system = platform.system()
88
89    option.cache_dir = tempfile.mkdtemp(prefix='unit-test-cache-')
90    public_dir(option.cache_dir)
91
92    # set stdout to non-blocking
93
94    if option.detailed or option.print_log:
95        fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0)
96
97
98def pytest_generate_tests(metafunc):
99    cls = metafunc.cls
100    if (not hasattr(cls, 'application_type')
101            or cls.application_type == None
102            or cls.application_type == 'external'):
103        return
104
105    type = cls.application_type
106
107    def generate_tests(versions):
108        metafunc.fixturenames.append('tmp_ct')
109        metafunc.parametrize('tmp_ct', versions)
110
111        for version in versions:
112            option.generated_tests[
113                metafunc.function.__name__ + '[{}]'.format(version)
114            ] = (type + ' ' + version)
115
116    # take available module from option and generate tests for each version
117
118    for module, prereq_version in cls.prerequisites['modules'].items():
119        if module in option.available['modules']:
120            available_versions = option.available['modules'][module]
121
122            if prereq_version == 'all':
123                generate_tests(available_versions)
124
125            elif prereq_version == 'any':
126                option.generated_tests[metafunc.function.__name__] = (
127                    type + ' ' + available_versions[0]
128                )
129            elif callable(prereq_version):
130                generate_tests(
131                    list(filter(prereq_version, available_versions))
132                )
133
134            else:
135                raise ValueError(
136                    """
137Unexpected prerequisite version "%s" for module "%s" in %s.
138'all', 'any' or callable expected."""
139                    % (str(prereq_version), module, str(cls))
140                )
141
142
143def pytest_sessionstart(session):
144    option.available = {'modules': {}, 'features': {}}
145
146    unit = unit_run()
147    option.temp_dir = unit['temp_dir']
148
149    # read unit.log
150
151    for i in range(50):
152        with open(unit['temp_dir'] + '/unit.log', 'r') as f:
153            log = f.read()
154            m = re.search('controller started', log)
155
156            if m is None:
157                time.sleep(0.1)
158            else:
159                break
160
161    if m is None:
162        _print_log(log)
163        exit("Unit is writing log too long")
164
165    # discover available modules from unit.log
166
167    for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M):
168        versions = option.available['modules'].setdefault(module[0], [])
169        if module[1] not in versions:
170            versions.append(module[1])
171
172    # discover modules from check
173
174    option.available['modules']['openssl'] = check_openssl(unit['unitd'])
175    option.available['modules']['go'] = check_go(
176        option.current_dir, unit['temp_dir'], option.test_dir
177    )
178    option.available['modules']['node'] = check_node(option.current_dir)
179
180    # remove None values
181
182    option.available['modules'] = {
183        k: v for k, v in option.available['modules'].items() if v is not None
184    }
185
186    check_isolation()
187
188    _clear_conf(unit['temp_dir'] + '/control.unit.sock')
189
190    unit_stop()
191
192    _check_alerts()
193
194    if option.restart:
195        shutil.rmtree(unit_instance['temp_dir'])
196
197    elif option.save_log:
198        open(unit_instance['temp_dir'] + '/' + unit_log_copy, 'w').close()
199
200@pytest.hookimpl(tryfirst=True, hookwrapper=True)
201def pytest_runtest_makereport(item, call):
202    # execute all other hooks to obtain the report object
203    outcome = yield
204    rep = outcome.get_result()
205
206    # set a report attribute for each phase of a call, which can
207    # be "setup", "call", "teardown"
208
209    setattr(item, "rep_" + rep.when, rep)
210
211
212@pytest.fixture(scope='class', autouse=True)
213def check_prerequisites(request):
214    cls = request.cls
215    missed = []
216
217    # check modules
218
219    if 'modules' in cls.prerequisites:
220        available_modules = list(option.available['modules'].keys())
221
222        for module in cls.prerequisites['modules']:
223            if module in available_modules:
224                continue
225
226            missed.append(module)
227
228    if missed:
229        pytest.skip('Unit has no ' + ', '.join(missed) + ' module(s)')
230
231    # check features
232
233    if 'features' in cls.prerequisites:
234        available_features = list(option.available['features'].keys())
235
236        for feature in cls.prerequisites['features']:
237            if feature in available_features:
238                continue
239
240            missed.append(feature)
241
242    if missed:
243        pytest.skip(', '.join(missed) + ' feature(s) not supported')
244
245
246@pytest.fixture(autouse=True)
247def run(request):
248    unit = unit_run()
249    option.temp_dir = unit['temp_dir']
250
251    option.skip_alerts = [
252        r'read signalfd\(4\) failed',
253        r'sendmsg.+failed',
254        r'recvmsg.+failed',
255    ]
256    option.skip_sanitizer = False
257
258    yield
259
260    # stop unit
261
262    error_stop_unit = unit_stop()
263    error_stop_processes = stop_processes()
264
265    # prepare log
266
267    with open(
268        unit_instance['log'], 'r', encoding='utf-8', errors='ignore'
269    ) as f:
270        log = f.read()
271
272    if not option.restart and option.save_log:
273        with open(unit_instance['temp_dir'] + '/' + unit_log_copy, 'a') as f:
274            f.write(log)
275
276    # remove unit.log
277
278    if not option.save_log and option.restart:
279        shutil.rmtree(unit['temp_dir'])
280
281    # clean temp_dir before the next test
282
283    if not option.restart:
284        _clear_conf(unit['temp_dir'] + '/control.unit.sock', log)
285
286        open(unit['log'], 'w').close()
287
288        for item in os.listdir(unit['temp_dir']):
289            if item not in [
290                'control.unit.sock',
291                'state',
292                'unit.pid',
293                'unit.log',
294                unit_log_copy,
295            ]:
296                path = os.path.join(unit['temp_dir'], item)
297
298                public_dir(path)
299
300                if os.path.isfile(path) or stat.S_ISSOCK(os.stat(path).st_mode):
301                    os.remove(path)
302                else:
303                    shutil.rmtree(path)
304
305    # print unit.log in case of error
306
307    if hasattr(request.node, 'rep_call') and request.node.rep_call.failed:
308        _print_log(log)
309
310    if error_stop_unit or error_stop_processes:
311        _print_log(log)
312
313    # check unit.log for errors
314
315    assert error_stop_unit is None, 'stop unit'
316    assert error_stop_processes is None, 'stop processes'
317
318    _check_alerts(log=log)
319
320def unit_run():
321    global unit_instance
322
323    if not option.restart and 'unitd' in unit_instance:
324        return unit_instance
325
326    build_dir = option.current_dir + '/build'
327    unitd = build_dir + '/unitd'
328
329    if not os.path.isfile(unitd):
330        exit('Could not find unit')
331
332    temp_dir = tempfile.mkdtemp(prefix='unit-test-')
333    public_dir(temp_dir)
334
335    if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777':
336        public_dir(build_dir)
337
338    os.mkdir(temp_dir + '/state')
339
340    unitd_args = [
341        unitd,
342        '--no-daemon',
343        '--modules',
344        build_dir,
345        '--state',
346        temp_dir + '/state',
347        '--pid',
348        temp_dir + '/unit.pid',
349        '--log',
350        temp_dir + '/unit.log',
351        '--control',
352        'unix:' + temp_dir + '/control.unit.sock',
353        '--tmp',
354        temp_dir,
355    ]
356
357    if option.user:
358        unitd_args.extend(['--user', option.user])
359
360    with open(temp_dir + '/unit.log', 'w') as log:
361        unit_instance['process'] = subprocess.Popen(unitd_args, stderr=log)
362
363    if not waitforfiles(temp_dir + '/control.unit.sock'):
364        _print_log()
365        exit('Could not start unit')
366
367    unit_instance['temp_dir'] = temp_dir
368    unit_instance['log'] = temp_dir + '/unit.log'
369    unit_instance['control_sock'] = temp_dir + '/control.unit.sock'
370    unit_instance['unitd'] = unitd
371
372    return unit_instance
373
374
375def unit_stop():
376    if not option.restart:
377        if inspect.stack()[1].function.startswith('test_'):
378            pytest.skip('no restart mode')
379
380        return
381
382    p = unit_instance['process']
383
384    if p.poll() is not None:
385        return
386
387    p.send_signal(signal.SIGQUIT)
388
389    try:
390        retcode = p.wait(15)
391        if retcode:
392            return 'Child process terminated with code ' + str(retcode)
393
394    except KeyboardInterrupt:
395        p.kill()
396        raise
397
398    except:
399        p.kill()
400        return 'Could not terminate unit'
401
402
403
404def _check_alerts(path=None, log=None):
405    if path is None:
406        path = unit_instance['log']
407
408    if log is None:
409        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
410            log = f.read()
411
412    found = False
413
414    alerts = re.findall(r'.+\[alert\].+', log)
415
416    if alerts:
417        print('\nAll alerts/sanitizer errors found in log:')
418        [print(alert) for alert in alerts]
419        found = True
420
421    if option.skip_alerts:
422        for skip in option.skip_alerts:
423            alerts = [al for al in alerts if re.search(skip, al) is None]
424
425    if alerts:
426        _print_log(log)
427        assert not alerts, 'alert(s)'
428
429    if not option.skip_sanitizer:
430        sanitizer_errors = re.findall('.+Sanitizer.+', log)
431
432        if sanitizer_errors:
433            _print_log(log)
434            assert not sanitizer_errors, 'sanitizer error(s)'
435
436    if found:
437        print('skipped.')
438
439
440def _print_log(data=None):
441    path = unit_instance['log']
442
443    print('Path to unit.log:\n' + path + '\n')
444
445    if option.print_log:
446        os.set_blocking(sys.stdout.fileno(), True)
447        sys.stdout.flush()
448
449        if data is None:
450            with open(path, 'r', encoding='utf-8', errors='ignore') as f:
451                shutil.copyfileobj(f, sys.stdout)
452        else:
453            sys.stdout.write(data)
454
455
456def _clear_conf(sock, log=None):
457    def check_success(resp):
458        if 'success' not in resp:
459            _print_log(log)
460            assert 'success' in resp
461
462    resp = http.put(
463        url='/config',
464        sock_type='unix',
465        addr=sock,
466        body=json.dumps({"listeners": {}, "applications": {}}),
467    )['body']
468
469    check_success(resp)
470
471    try:
472        certs = json.loads(http.get(
473            url='/certificates',
474            sock_type='unix',
475            addr=sock,
476        )['body']).keys()
477
478    except json.JSONDecodeError:
479        pytest.fail('Can\'t parse certificates list.')
480
481    for cert in certs:
482        resp = http.delete(
483            url='/certificates/' + cert,
484            sock_type='unix',
485            addr=sock,
486        )['body']
487
488        check_success(resp)
489
490def run_process(target, *args):
491    global _processes
492
493    process = Process(target=target, args=args)
494    process.start()
495
496    _processes.append(process)
497
498def stop_processes():
499    if not _processes:
500        return
501
502    fail = False
503    for process in _processes:
504        if process.is_alive():
505            process.terminate()
506            process.join(timeout=15)
507
508            if process.is_alive():
509                fail = True
510
511    if fail:
512        return 'Fail to stop process(es)'
513
514
515@pytest.fixture()
516def skip_alert():
517    def _skip(*alerts):
518        option.skip_alerts.extend(alerts)
519
520    return _skip
521
522
523@pytest.fixture
524def temp_dir(request):
525    return unit_instance['temp_dir']
526
527@pytest.fixture
528def is_unsafe(request):
529    return request.config.getoption("--unsafe")
530
531@pytest.fixture
532def is_su(request):
533    return os.geteuid() == 0
534
535@pytest.fixture
536def unit_pid(request):
537    return unit_instance['process'].pid
538
539def pytest_sessionfinish(session):
540    if not option.restart and option.save_log:
541        print('Path to unit.log:\n' + unit_instance['log'] + '\n')
542
543    option.restart = True
544
545    unit_stop()
546    shutil.rmtree(option.cache_dir)
547