source: mod_gnutls/test/mgstest/services.py @ 99c61f9

proxy-ticket
Last change on this file since 99c61f9 was 99c61f9, checked in by Fiona Klute <fiona.klute@…>, 4 months ago

Add configure option --enable-valgrind-test to run tests with Valgrind

Also includes suppressions for known issues not caused by mod_gnutls.

  • Property mode set to 100644
File size: 6.4 KB
Line 
1# Copyright 2019 Fiona Klute
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Handling services needed for mod_gnutls tests"""
16
17import errno
18import os
19import signal
20import subprocess
21import sys
22
23from contextlib import contextmanager
24from pathlib import Path
25from time import sleep
26
27class TestService:
28    """A generic service used in the mod_gnutls test environment."""
29
30    def __init__(self, start=None, stop=None, env=None,
31                 condition=None, check=None, pidfile=None):
32        # command to start the service
33        self.start_command = start
34        # command to stop the service (otherwise use SIGTERM)
35        self.stop_command = stop
36        # condition: start service if the function returns true
37        self.condition = condition or (lambda: True)
38
39        # child process
40        self.process = None
41        # will contain the return code of the child process after
42        # successful wait()
43        self.returncode = None
44        # PID file, if any. The process must delete its PID file when
45        # exiting.
46        self.pidfile = Path(pidfile) if pidfile else None
47
48        # add environment variables for a subprocess only
49        if env:
50            self.process_env = os.environ.copy()
51            for name, value in env.items():
52                self.process_env[name] = value
53        else:
54            self.process_env = None
55
56        # check: function to check if the service is up and working
57        self.check = check
58
59        # sleep step for waiting (sec)
60        self._step = int(os.environ.get('TEST_SERVICE_WAIT', 250)) / 1000
61
62    def start(self):
63        """Start the service"""
64        if not self.condition():
65            # skip
66            return
67        print(f'Starting: {self.start_command}')
68        self.process = subprocess.Popen(self.start_command,
69                                        env=self.process_env,
70                                        close_fds=True)
71        self.returncode = None
72
73    def stop(self):
74        """Order the service to stop"""
75        if not self.condition():
76            # skip
77            return
78        if not self.process or self.process.poll():
79            # process either never started or already stopped
80            return
81
82        if self.stop_command:
83            print(f'Stopping: {self.stop_command}')
84            subprocess.run(self.stop_command, check=True, env=self.process_env)
85        else:
86            print(f'Stopping (SIGTERM): {self.start_command}')
87            self.process.terminate()
88
89    def wait(self, timeout=None):
90        """Wait for the process to terminate.
91
92        Sets returncode to the process' return code and returns it.
93
94        WARNING: Calling this method without a timeout or calling
95        stop() first will hang. An expired timeout will raise a
96        subprocess.TimeoutExpired exception.
97
98        """
99        if self.process:
100            self.process.wait(timeout=timeout)
101            self.returncode = self.process.returncode
102            self.process = None
103            return self.returncode
104
105    def wait_ready(self, timeout=None):
106        """Wait for the started service to be ready.
107
108        The function passed to the constructor as "check" is called to
109        determine whether it is. Waiting also ends if self.process
110        terminates.
111
112        Returns: None if the service is ready, or the return code if
113        the process has terminated.
114
115        Raises a TimeoutError if the given timeout has been exceeded.
116
117        """
118        if not self.check:
119            return None
120
121        slept = 0
122        while not timeout or slept < timeout:
123            if self.process and self.process.poll():
124                return self.process.returncode
125            if self.check():
126                return None
127            else:
128                sleep(self._step)
129                slept = slept + self._step
130        # TODO: A custom ServiceException or something would be nicer
131        # here.
132        raise TimeoutError('Waiting for service timed out!')
133
134    @contextmanager
135    def run(self):
136        """Context manager to start and stop a service. Note that entering the
137        context does not call TestService.wait_ready() on the service,
138        you must do that separately if desired.
139
140        """
141        try:
142            self.start()
143            # TODO: with async execution we could also call
144            # wait_ready() here
145            yield self
146        finally:
147            self.stop()
148            # TODO: this would really benefit from async execution
149            self.wait()
150
151
152
153class ApacheService(TestService):
154    """An Apache HTTPD instance used in the mod_gnutls test
155    environment."""
156
157    apache2 = os.environ.get('APACHE2', 'apache2')
158
159    def __init__(self, config, env=None, pidfile=None, check=None,
160                 valgrind_log=None, valgrind_suppress=[]):
161        self.config = Path(config).resolve()
162        base_cmd = [self.apache2, '-f', str(self.config), '-k']
163        start_cmd = base_cmd + ['start', '-DFOREGROUND']
164        if valgrind_log:
165            valgrind = os.environ.get('VALGRIND', 'valgrind')
166            suppress = [f'--suppressions={s}' for s in valgrind_suppress]
167            start_cmd = [valgrind, '-s', '--leak-check=full',
168                         '--track-origins=yes', '--vgdb=no',
169                         f'--log-file={valgrind_log}'] \
170                         + suppress + start_cmd
171        if not check:
172            check = self.pidfile_check
173        super(ApacheService, self).__init__(start=start_cmd,
174                                            stop=base_cmd + ['stop'],
175                                            env=env,
176                                            pidfile=pidfile,
177                                            condition=self.config_exists,
178                                            check=check)
179
180    def config_exists(self):
181        return self.config.is_file()
182
183    def pidfile_check(self):
184        """Default check method for ApacheService, waits for the PID file to
185        be present."""
186        return self.pidfile.is_file()
Note: See TracBrowser for help on using the repository browser.