source: mod_gnutls/test/mgstest/services.py @ 60a415a

asyncio
Last change on this file since 60a415a was 60a415a, checked in by Fiona Klute <fiona.klute@…>, 2 years ago

Fix simple formatting issues reported by flake8

  • 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 os
18import subprocess
19
20from contextlib import contextmanager
21from pathlib import Path
22from time import sleep
23
24
25class TestService:
26    """A generic service used in the mod_gnutls test environment."""
27
28    def __init__(self, start=None, stop=None, env=None,
29                 condition=None, check=None, pidfile=None):
30        # command to start the service
31        self.start_command = start
32        # command to stop the service (otherwise use SIGTERM)
33        self.stop_command = stop
34        # condition: start service if the function returns true
35        self.condition = condition or (lambda: True)
36
37        # child process
38        self.process = None
39        # will contain the return code of the child process after
40        # successful wait()
41        self.returncode = None
42        # PID file, if any. The process must delete its PID file when
43        # exiting.
44        self.pidfile = Path(pidfile) if pidfile else None
45
46        # add environment variables for a subprocess only
47        if env:
48            self.process_env = os.environ.copy()
49            for name, value in env.items():
50                self.process_env[name] = value
51        else:
52            self.process_env = None
53
54        # check: function to check if the service is up and working
55        self.check = check
56
57        # sleep step for waiting (sec)
58        self._step = int(os.environ.get('TEST_SERVICE_WAIT', 250)) / 1000
59
60    def start(self):
61        """Start the service"""
62        if not self.condition():
63            # skip
64            return
65        print(f'Starting: {self.start_command}')
66        self.process = subprocess.Popen(self.start_command,
67                                        env=self.process_env,
68                                        close_fds=True)
69        self.returncode = None
70
71    def stop(self):
72        """Order the service to stop"""
73        if not self.condition():
74            # skip
75            return
76        if not self.process or self.process.poll():
77            # process either never started or already stopped
78            return
79
80        if self.stop_command:
81            print(f'Stopping: {self.stop_command}')
82            subprocess.run(self.stop_command, check=True, env=self.process_env)
83        else:
84            print(f'Stopping (SIGTERM): {self.start_command}')
85            self.process.terminate()
86
87    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 a timeout or calling
93        stop() first will hang. An expired timeout will raise a
94        subprocess.TimeoutExpired exception.
95
96        """
97        if self.process:
98            self.process.wait(timeout=timeout)
99            self.returncode = self.process.returncode
100            self.process = None
101            return self.returncode
102
103    def wait_ready(self, timeout=None):
104        """Wait for the started service to be ready.
105
106        The function passed to the constructor as "check" is called to
107        determine whether it is. Waiting also ends if self.process
108        terminates.
109
110        Returns: None if the service is ready, or the return code if
111        the process has terminated.
112
113        Raises a TimeoutError if the given timeout has been exceeded.
114
115        """
116        if not self.check:
117            return None
118
119        slept = 0
120        while not timeout or slept < timeout:
121            if self.process and self.process.poll():
122                return self.process.returncode
123            if self.check():
124                return None
125            else:
126                sleep(self._step)
127                slept = slept + self._step
128        # TODO: A custom ServiceException or something would be nicer
129        # here.
130        raise TimeoutError('Waiting for service timed out!')
131
132    @contextmanager
133    def run(self):
134        """Context manager to start and stop a service. Note that entering the
135        context does not call TestService.wait_ready() on the service,
136        you must do that separately if desired.
137
138        """
139        try:
140            self.start()
141            # TODO: with async execution we could also call
142            # wait_ready() here
143            yield self
144        finally:
145            self.stop()
146            # TODO: this would really benefit from async execution
147            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.