source: mod_gnutls/test/runtest.py @ 883efd2

asyncioproxy-ticket
Last change on this file since 883efd2 was 005b185, checked in by Fiona Klute <fiona.klute@…>, 18 months ago

Fail tests running with Valgrind if Valgrind reports errors

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