source: mod_gnutls/test/mgstest/services.py @ 7eb4233

asyncio
Last change on this file since 7eb4233 was 7eb4233, checked in by Fiona Klute <fiona.klute@…>, 3 months ago

Use asyncio to manage test services

This makes it a little easier to handle the services in parallel.

  • Property mode set to 100644
File size: 6.5 KB
Line 
1# Copyright 2019-2020 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 asyncio
18import os
19
20from contextlib import asynccontextmanager
21from pathlib import Path
22
23
24class TestService:
25    """A generic service used in the mod_gnutls test environment."""
26
27    def __init__(self, start=None, stop=None, env=None,
28                 condition=None, check=None, pidfile=None):
29        # command to start the service
30        self.start_command = start
31        # command to stop the service (otherwise use SIGTERM)
32        self.stop_command = stop
33        # condition: start service if the function returns true
34        self.condition = condition or (lambda: True)
35
36        # child process
37        self.process = None
38        # will contain the return code of the child process after
39        # successful wait()
40        self.returncode = None
41        # PID file, if any. The process must delete its PID file when
42        # exiting.
43        self.pidfile = Path(pidfile) if pidfile else None
44
45        # add environment variables for a subprocess only
46        if env:
47            self.process_env = os.environ.copy()
48            for name, value in env.items():
49                self.process_env[name] = value
50        else:
51            self.process_env = None
52
53        # check: function to check if the service is up and working
54        self.check = check
55
56        # sleep step for waiting (sec)
57        self._step = int(os.environ.get('TEST_SERVICE_WAIT', 250)) / 1000
58
59    async def start(self):
60        """Start the service"""
61        if not self.condition():
62            # skip
63            return
64        print(f'Starting: {self.start_command}')
65        self.process = await asyncio.create_subprocess_exec(
66            *self.start_command, env=self.process_env, close_fds=True)
67        self.returncode = None
68
69    async def stop(self):
70        """Order the service to stop"""
71        if not self.condition():
72            # skip
73            return
74        if not self.process or self.process.returncode is not None:
75            # process either never started or already stopped
76            return
77
78        if self.stop_command:
79            print(f'Stopping: {self.stop_command}')
80            stop = await asyncio.create_subprocess_exec(
81                *self.stop_command, env=self.process_env)
82            await stop.wait()
83        else:
84            print(f'Stopping (SIGTERM): {self.start_command}')
85            self.process.terminate()
86
87    async def wait(self, timeout=None):
88        """Wait for the process to terminate.
89
90        Sets returncode to the process' return code and returns it.
91
92        WARNING: Calling this method without calling stop() first will
93        hang, unless the service stops on its own.
94
95        """
96        if self.process:
97            await self.process.wait()
98            self.returncode = self.process.returncode
99            self.process = None
100            return self.returncode
101
102    async def wait_ready(self, timeout=None):
103        """Wait for the started service to be ready.
104
105        The function passed to the constructor as "check" is called to
106        determine whether it is. Waiting also ends if self.process
107        terminates.
108
109        Returns: None if the service is ready, or the return code if
110        the process has terminated.
111
112        Raises a TimeoutError if the given timeout has been exceeded.
113
114        """
115        if not self.condition():
116            # skip
117            return None
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.returncode is not None:
124                return self.process.returncode
125            if self.check():
126                return None
127            else:
128                await asyncio.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    @asynccontextmanager
135    async def run(self, ready_timeout=None):
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            await self.start()
143            await self.wait_ready(timeout=ready_timeout)
144            yield self
145        finally:
146            await self.stop()
147            await self.wait()
148
149
150class ApacheService(TestService):
151    """An Apache HTTPD instance used in the mod_gnutls test
152    environment."""
153
154    apache2 = os.environ.get('APACHE2', 'apache2')
155
156    def __init__(self, config, env=None, pidfile=None, check=None,
157                 valgrind_log=None, valgrind_suppress=[]):
158        self.config = Path(config).resolve()
159        base_cmd = [self.apache2, '-f', str(self.config), '-k']
160        start_cmd = base_cmd + ['start', '-DFOREGROUND']
161        if valgrind_log:
162            valgrind = os.environ.get('VALGRIND', 'valgrind')
163            suppress = [f'--suppressions={s}' for s in valgrind_suppress]
164            start_cmd = [valgrind, '-v', '--leak-check=full',
165                         '--num-callers=20',
166                         '--gen-suppressions=all',
167                         '--keep-debuginfo=yes',
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.