source: mod_gnutls/test/mgstest/services.py

Last change on this file was 514058d, checked in by Fiona Klute <fiona.klute@…>, 8 months ago

Define service wait times as seconds

Python needs them that way anyway, an they aren't used anywhere else.

  • Property mode set to 100644
File size: 6.1 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):
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
42        # add environment variables for a subprocess only
43        if env:
44            self.process_env = os.environ.copy()
45            for name, value in env.items():
46                self.process_env[name] = value
47        else:
48            self.process_env = None
49
50        # check: coroutine to check if the service is up and working
51        self.check = check
52
53        # sleep step for waiting (sec)
54        self._step = float(os.environ.get('TEST_SERVICE_WAIT', 0.25))
55
56    async def start(self):
57        """Start the service"""
58        if not self.condition():
59            # skip
60            return
61        print(f'Starting: {self.start_command}')
62        self.process = await asyncio.create_subprocess_exec(
63            *self.start_command, env=self.process_env, close_fds=True)
64        self.returncode = None
65
66    async def stop(self):
67        """Order the service to stop"""
68        if not self.condition():
69            # skip
70            return
71        if not self.process or self.process.returncode is not None:
72            # process either never started or already stopped
73            return
74
75        if self.stop_command:
76            print(f'Stopping: {self.stop_command}')
77            stop = await asyncio.create_subprocess_exec(
78                *self.stop_command, env=self.process_env)
79            await stop.wait()
80        else:
81            print(f'Stopping (SIGTERM): {self.start_command}')
82            self.process.terminate()
83
84    async def wait(self):
85        """Wait for the process to terminate.
86
87        Sets returncode to the process' return code and returns it.
88
89        WARNING: Calling this method without calling stop() first will
90        hang, unless the service stops on its own. Wrap in
91        asyncio.wait_for() as needed.
92
93        """
94        if self.process:
95            await self.process.wait()
96            self.returncode = self.process.returncode
97            self.process = None
98            return self.returncode
99
100    async def wait_ready(self):
101        """Wait for the started service to be ready.
102
103        The function passed to the constructor as "check" is called to
104        determine whether it is. Waiting also ends if self.process
105        terminates.
106
107        Returns: None if the service is ready, or the return code if
108        the process has terminated.
109
110        """
111        if not self.condition():
112            # skip
113            return None
114        if not self.check:
115            return None
116
117        while True:
118            if self.process and self.process.returncode is not None:
119                return self.process.returncode
120            if await self.check():
121                return None
122            else:
123                await asyncio.sleep(self._step)
124
125    @asynccontextmanager
126    async def run(self, ready_timeout=None):
127        """Context manager to start and stop a service. Yields when the
128        service is ready.
129
130        """
131        try:
132            await self.start()
133            await asyncio.wait_for(self.wait_ready(), timeout=ready_timeout)
134            yield self
135        finally:
136            await self.stop()
137            await self.wait()
138
139
140class ApacheService(TestService):
141    """An Apache HTTPD instance used in the mod_gnutls test
142    environment."""
143
144    apache2 = os.environ.get('APACHE2', 'apache2')
145
146    def __init__(self, config, pidfile, env=None, check=None,
147                 valgrind_log=None, valgrind_suppress=[]):
148        self.config = Path(config).resolve()
149        # PID file, used by default to check if the server is up.
150        self.pidfile = Path(pidfile)
151        base_cmd = [self.apache2, '-f', str(self.config), '-k']
152        start_cmd = base_cmd + ['start', '-DFOREGROUND']
153        if valgrind_log:
154            valgrind = os.environ.get('VALGRIND', 'valgrind')
155            suppress = [f'--suppressions={s}' for s in valgrind_suppress]
156            start_cmd = [valgrind, '-v', '--leak-check=full',
157                         '--num-callers=20',
158                         '--gen-suppressions=all',
159                         '--keep-debuginfo=yes',
160                         '--track-origins=yes', '--vgdb=no',
161                         f'--log-file={valgrind_log}'] \
162                + suppress + start_cmd
163        if not check:
164            check = self.pidfile_check
165        super(ApacheService, self).__init__(start=start_cmd,
166                                            stop=base_cmd + ['stop'],
167                                            env=env,
168                                            condition=self.config_exists,
169                                            check=check)
170
171    def config_exists(self):
172        return self.config.is_file()
173
174    async def pidfile_check(self):
175        """Default check method for ApacheService, waits for the PID file to
176        be present."""
177        return self.pidfile.is_file()
Note: See TracBrowser for help on using the repository browser.