source: mod_gnutls/test/runtest.py @ 9231a4d

Last change on this file since 9231a4d was 9231a4d, checked in by Fiona Klute <fiona.klute@…>, 2 years ago

Send stop signal to services before leaving their AsyncExitStack?

The AsyncExitStack? exits its contexts one after the other, even for
async contextmanagers. Sending the stop signal to services before
leaving should speed up tests a little. Note that this only happens if
the async with block isn't left because of an unhandled exception.

  • Property mode set to 100755
File size: 9.7 KB
Line 
1#!/usr/bin/python3
2# PYTHON_ARGCOMPLETE_OK
3
4# Copyright 2019-2020 Fiona Klute
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import asyncio
19import contextlib
20import itertools
21import os
22import os.path
23import subprocess
24import sys
25import tempfile
26import yaml
27from unittest import SkipTest
28
29import mgstest.hooks
30import mgstest.valgrind
31from mgstest import lockfile, TestExpectationFailed
32from mgstest.services import ApacheService, TestService
33from mgstest.tests import run_test_conf
34
35
36def find_testdir(number, dir):
37    """Find the configuration directory for a test based on its
38    number. The given directory must contain exactly one directory
39    with a name matching "NUMBER_*", otherwise a LookupError is
40    raised.
41
42    """
43    with os.scandir(dir) as it:
44        found = None
45        for entry in it:
46            if entry.is_dir():
47                num = int(entry.name.split('_', maxsplit=1)[0])
48                if number == num:
49                    if found:
50                        # duplicate numbers are an error
51                        raise LookupError('Multiple directories found for '
52                                          f'test number {number}: '
53                                          f'{found.name} and {entry.name}')
54                    else:
55                        found = entry
56        if found is None:
57            raise LookupError('No test directory found for test number '
58                              f'{number}!')
59        else:
60            return (found.path, found.name)
61
62
63def temp_logfile():
64    return tempfile.SpooledTemporaryFile(max_size=4096, mode='w+',
65                                         prefix='mod_gnutls', suffix=".log")
66
67
68async def check_ocsp_responder():
69    # Check if OCSP responder works
70    issuer_cert = 'authority/x509.pem'
71    check_cert = 'authority/server/x509.pem'
72    proc = await asyncio.create_subprocess_exec(
73        'ocsptool', '--ask', '--nonce',
74        '--load-issuer', issuer_cert, '--load-cert', check_cert)
75    return (await proc.wait()) == 0
76
77
78async def check_msva():
79    # Check if MSVA is up
80    cert_file = 'authority/client/x509.pem'
81    uid_file = 'authority/client/uid'
82    with open(uid_file, 'r') as file:
83        uid = file.read().strip()
84    with open(cert_file, 'r') as cert:
85        proc = await asyncio.create_subprocess_exec(
86            'msva-query-agent', 'https', uid, 'x509pem', 'client',
87            stdin=cert)
88        return (await proc.wait()) == 0
89
90
91async def main(args):
92    # The Automake environment always provides srcdir, the default is
93    # for manual use.
94    srcdir = os.path.realpath(os.environ.get('srcdir', '.'))
95    # ensure environment srcdir is absolute
96    os.environ['srcdir'] = srcdir
97
98    # Find the configuration directory for the test in
99    # ${srcdir}/tests/, based on the test number.
100    testdir, testname = find_testdir(args.test_number,
101                                     os.path.join(srcdir, 'tests'))
102    print(f'Found test {testname}, test dir is {testdir}')
103    os.environ['TEST_NAME'] = testname
104
105    # Load test config
106    try:
107        with open(os.path.join(testdir, 'test.yaml'), 'r') as conf_file:
108            test_conf = yaml.load(conf_file, Loader=yaml.Loader)
109    except FileNotFoundError:
110        test_conf = None
111
112    # Load test case hooks (if any)
113    plugin_path = os.path.join(testdir, 'hooks.py')
114    plugin = mgstest.hooks.load_hooks_plugin(plugin_path)
115
116    # PID file name varies depending on whether we're using
117    # namespaces.
118    #
119    # TODO: Check if having the different names is really necessary.
120    pidaffix = ''
121    if 'USE_TEST_NAMESPACE' in os.environ:
122        pidaffix = f'-{testname}'
123
124    valgrind_log = None
125    if args.valgrind:
126        valgrind_log = os.path.join('logs', f'valgrind-{testname}.log')
127
128    # Define the available services
129    apache = ApacheService(config=os.path.join(testdir, 'apache.conf'),
130                           pidfile=f'apache2{pidaffix}.pid',
131                           valgrind_log=valgrind_log,
132                           valgrind_suppress=args.valgrind_suppressions)
133    backend = ApacheService(config=os.path.join(testdir, 'backend.conf'),
134                            pidfile=f'backend{pidaffix}.pid')
135    ocsp = ApacheService(config=os.path.join(testdir, 'ocsp.conf'),
136                         pidfile=f'ocsp{pidaffix}.pid',
137                         check=check_ocsp_responder)
138    msva = TestService(start=['monkeysphere-validation-agent'],
139                       env={'GNUPGHOME': 'msva.gnupghome',
140                            'MSVA_KEYSERVER_POLICY': 'never'},
141                       condition=lambda: 'USE_MSVA' in os.environ,
142                       check=check_msva)
143
144    # background services: must be ready before the main apache
145    # instance is started
146    bg_services = [backend, ocsp, msva]
147
148    # If VERBOSE is enabled, log the HTTPD build configuration
149    if 'VERBOSE' in os.environ:
150        apache2 = os.environ.get('APACHE2', 'apache2')
151        subprocess.run([apache2, '-f', f'{srcdir}/base_apache.conf', '-V'],
152                       check=True)
153
154    # This hook may modify the environment as needed for the test.
155    cleanup_callback = None
156    try:
157        if plugin.prepare_env:
158            cleanup_callback = plugin.prepare_env()
159    except SkipTest as skip:
160        print(f'Skipping: {skip!s}')
161        sys.exit(77)
162
163    if 'USE_MSVA' in os.environ:
164        os.environ['MONKEYSPHERE_VALIDATION_AGENT_SOCKET'] = \
165            f'http://127.0.0.1:{os.environ["MSVA_PORT"]}'
166
167    async with contextlib.AsyncExitStack() as service_stack:
168        if cleanup_callback:
169            service_stack.callback(cleanup_callback)
170        service_stack.enter_context(
171            lockfile('test.lock', nolock='MGS_NETNS_ACTIVE' in os.environ))
172
173        # TEST_SERVICE_MAX_WAIT is in milliseconds
174        wait_timeout = \
175            int(os.environ.get('TEST_SERVICE_MAX_WAIT', 10000)) / 1000
176        await asyncio.wait(
177            {asyncio.create_task(
178                service_stack.enter_async_context(
179                    s.run(ready_timeout=wait_timeout)))
180             for s in bg_services})
181
182        # special case: expected to fail in a few cases
183        await service_stack.enter_async_context(apache.run())
184        failed = await apache.wait_ready()
185        if os.path.exists(os.path.join(testdir, 'fail.server')):
186            if failed:
187                print('Apache server failed to start as expected',
188                      file=sys.stderr)
189            else:
190                raise TestExpectationFailed(
191                    'Server start did not fail as expected!')
192
193        # Set TEST_TARGET for the request. Might be replaced with a
194        # parameter later.
195        if 'TARGET_IP' in os.environ:
196            os.environ['TEST_TARGET'] = os.environ['TARGET_IP']
197        else:
198            os.environ['TEST_TARGET'] = os.environ['TEST_HOST']
199
200        # Run the test connections
201        if plugin.run_connection:
202            plugin.run_connection(testname,
203                                  conn_log=args.log_connection,
204                                  response_log=args.log_responses)
205        else:
206            run_test_conf(test_conf,
207                          float(os.environ.get('TEST_QUERY_TIMEOUT', 5.0)),
208                          conn_log=args.log_connection,
209                          response_log=args.log_responses)
210
211        await asyncio.wait(
212            {asyncio.create_task(s.stop())
213             for s in itertools.chain([apache], bg_services)})
214
215    # run extra checks the test's hooks.py might define
216    if plugin.post_check:
217        args.log_connection.seek(0)
218        args.log_responses.seek(0)
219        plugin.post_check(conn_log=args.log_connection,
220                          response_log=args.log_responses)
221
222    if valgrind_log:
223        with open(valgrind_log) as log:
224            errors = mgstest.valgrind.error_summary(log)
225            print(f'Valgrind summary: {errors[0]} errors, '
226                  f'{errors[1]} suppressed')
227            if errors[0] > 0:
228                sys.exit(ord('V'))
229
230
231if __name__ == "__main__":
232    import argparse
233    parser = argparse.ArgumentParser(
234        description='Run a mod_gnutls server test')
235    parser.add_argument('--test-number', type=int,
236                        required=True, help='load YAML test configuration')
237    parser.add_argument('--log-connection', type=argparse.FileType('w+'),
238                        default=temp_logfile(),
239                        help='write connection log to this file')
240    parser.add_argument('--log-responses', type=argparse.FileType('w+'),
241                        default=temp_logfile(),
242                        help='write HTTP responses to this file')
243    parser.add_argument('--valgrind', action='store_true',
244                        help='run primary Apache instance with Valgrind')
245    parser.add_argument('--valgrind-suppressions', action='append',
246                        default=[],
247                        help='use Valgrind suppressions file')
248
249    # enable bash completion if argcomplete is available
250    try:
251        import argcomplete
252        argcomplete.autocomplete(parser)
253    except ImportError:
254        pass
255
256    args = parser.parse_args()
257
258    with contextlib.ExitStack() as stack:
259        stack.enter_context(contextlib.closing(args.log_connection))
260        stack.enter_context(contextlib.closing(args.log_responses))
261        asyncio.run(main(args))
Note: See TracBrowser for help on using the repository browser.