source: mod_gnutls/test/mgstest/services.py @ 422eade

asyncioproxy-ticket
Last change on this file since 422eade was 422eade, checked in by Fiona Klute <fiona.klute@…>, 15 months ago

Support optional timeout for TestService?.wait()

  • Property mode set to 100644
File size: 5.9 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        # 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    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 = subprocess.Popen(self.start_command,
66                                        env=self.process_env,
67                                        close_fds=True)
68
69    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.poll():
75            # process either never started or already stopped
76            return
77
78        if self.stop_command:
79            print(f'Stopping: {self.stop_command}')
80            subprocess.run(self.stop_command, check=True, env=self.process_env)
81        else:
82            print(f'Stopping (SIGTERM): {self.start_command}')
83            self.process.terminate()
84
85    def wait(self, timeout=None):
86        """Wait for the process to actually stop after calling stop().
87
88        WARNING: Calling this method without a timeout or calling
89        stop() first will hang. An expired timeout will raise a
90        subprocess.TimeoutExpired exception.
91
92        """
93        if self.process:
94            self.process.wait(timeout=timeout)
95            self.process = None
96
97    def wait_ready(self, timeout=None):
98        """Wait for the started service to be ready.
99
100        The function passed to the constructor as "check" is called to
101        determine whether it is. Waiting also ends if self.process
102        terminates.
103
104        Returns: None if the service is ready, or the return code if
105        the process has terminated.
106
107        Raises a TimeoutError if the given timeout has been exceeded.
108
109        """
110        if not self.check:
111            return None
112
113        slept = 0
114        while not timeout or slept < timeout:
115            if self.process and self.process.poll():
116                return self.process.returncode
117            if self.check():
118                return None
119            else:
120                sleep(self._step)
121                slept = slept + self._step
122        # TODO: A custom ServiceException or something would be nicer
123        # here.
124        raise TimeoutError('Waiting for service timed out!')
125
126    @contextmanager
127    def run(self):
128        """Context manager to start and stop a service. Note that entering the
129        context does not call TestService.wait_ready() on the service,
130        you must do that separately if desired.
131
132        """
133        try:
134            self.start()
135            # TODO: with async execution we could also call
136            # wait_ready() here
137            yield self
138        finally:
139            self.stop()
140            # TODO: this would really benefit from async execution
141            self.wait()
142
143
144
145class ApacheService(TestService):
146    """An Apache HTTPD instance used in the mod_gnutls test
147    environment."""
148
149    apache2 = os.environ.get('APACHE2', 'apache2')
150
151    def __init__(self, config, env=None, pidfile=None, check=None,
152                 valgrind_log=None):
153        self.config = Path(config).resolve()
154        base_cmd = [self.apache2, '-f', str(self.config), '-k']
155        start_cmd = base_cmd + ['start', '-DFOREGROUND']
156        if valgrind_log:
157            start_cmd = ['valgrind', '-s', '--leak-check=full',
158                         '--track-origins=yes', '--vgdb=no',
159                         f'--log-file={valgrind_log}'] \
160                         + start_cmd
161        if not check:
162            check = self.pidfile_check
163        super(ApacheService, self).__init__(start=start_cmd,
164                                            stop=base_cmd + ['stop'],
165                                            env=env,
166                                            pidfile=pidfile,
167                                            condition=self.config_exists,
168                                            check=check)
169
170    def config_exists(self):
171        return self.config.is_file()
172
173    def pidfile_check(self):
174        """Default check method for ApacheService, waits for the PID file to
175        be present."""
176        return self.pidfile.is_file()
Note: See TracBrowser for help on using the repository browser.