source: mod_gnutls/test/runtest.py @ 264ab17

asyncioproxy-ticket
Last change on this file since 264ab17 was e2200db, checked in by Fiona Klute <fiona.klute@…>, 15 months ago

Test suite: Always run Apache with "-DFOREGROUND"

This simplifies process management a lot.

  • Property mode set to 100755
File size: 8.8 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        service_stack.enter_context(apache.run())
172        failed = apache.wait_ready()
173        if os.path.exists(os.path.join(testdir, 'fail.server')):
174            if failed:
175                print('Apache server failed to start as expected',
176                      file=sys.stderr)
177            else:
178                raise TestExpectationFailed(
179                    'Server start did not fail as expected!')
180
181        # Set TEST_TARGET for the request. Might be replaced with a
182        # parameter later.
183        if 'TARGET_IP' in os.environ:
184            os.environ['TEST_TARGET'] = os.environ['TARGET_IP']
185        else:
186            os.environ['TEST_TARGET'] = os.environ['TEST_HOST']
187
188        # Run the test connections
189        if plugin.run_connection:
190            plugin.run_connection(testname,
191                                  conn_log=args.log_connection,
192                                  response_log=args.log_responses)
193        else:
194            with open(os.path.join(testdir, 'test.yml'), 'r') as test_conf:
195                run_test_conf(test_conf,
196                              float(os.environ.get('TEST_QUERY_TIMEOUT', 5.0)),
197                              conn_log=args.log_connection,
198                              response_log=args.log_responses)
199
200    # run extra checks the test's hooks.py might define
201    if plugin.post_check:
202        args.log_connection.seek(0)
203        args.log_responses.seek(0)
204        plugin.post_check(conn_log=args.log_connection,
205                          response_log=args.log_responses)
206
207
208
209if __name__ == "__main__":
210    import argparse
211    parser = argparse.ArgumentParser(
212        description='Run a mod_gnutls server test')
213    parser.add_argument('--test-number', type=int,
214                        required=True, help='load YAML test configuration')
215    parser.add_argument('--log-connection', type=argparse.FileType('w+'),
216                        default=temp_logfile(),
217                        help='write connection log to this file')
218    parser.add_argument('--log-responses', type=argparse.FileType('w+'),
219                        default=temp_logfile(),
220                        help='write HTTP responses to this file')
221    parser.add_argument('--valgrind', action='store_true',
222                        help='run primary Apache instance with Valgrind')
223
224    # enable bash completion if argcomplete is available
225    try:
226        import argcomplete
227        argcomplete.autocomplete(parser)
228    except ImportError:
229        pass
230
231    args = parser.parse_args()
232
233    with contextlib.ExitStack() as stack:
234        stack.enter_context(contextlib.closing(args.log_connection))
235        stack.enter_context(contextlib.closing(args.log_responses))
236        main(args)
Note: See TracBrowser for help on using the repository browser.