source: mod_gnutls/test/runtest.py @ 72ebe64

asyncioproxy-ticket
Last change on this file since 72ebe64 was 72ebe64, checked in by Fiona Klute <fiona.klute@…>, 23 months ago

runtest.py: Support for running Apache with Valgrind

Not used by "make check" yet.

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