##// END OF EJS Templates
tests: fixed starting of customized gunicorn with a explicit --bind call
super-admin -
r4976:6c82c462 default
parent child Browse files
Show More
@@ -1,285 +1,285 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22 import platform
23 23 import socket
24 24 import random
25 25 import pytest
26 26
27 27 from rhodecode.lib.pyramid_utils import get_app_config
28 28 from rhodecode.tests.fixture import TestINI
29 29 from rhodecode.tests.server_utils import RcVCSServer
30 30
31 31
32 32 def _parse_json(value):
33 33 return json.loads(value) if value else None
34 34
35 35
36 36 def pytest_addoption(parser):
37 37 parser.addoption(
38 38 '--test-loglevel', dest='test_loglevel',
39 39 help="Set default Logging level for tests, critical(default), error, warn , info, debug")
40 40 group = parser.getgroup('pylons')
41 41 group.addoption(
42 42 '--with-pylons', dest='pyramid_config',
43 43 help="Set up a Pylons environment with the specified config file.")
44 44 group.addoption(
45 45 '--ini-config-override', action='store', type=_parse_json,
46 46 default=None, dest='pyramid_config_override', help=(
47 47 "Overrides the .ini file settings. Should be specified in JSON"
48 48 " format, e.g. '{\"section\": {\"parameter\": \"value\", ...}}'"
49 49 )
50 50 )
51 51 parser.addini(
52 52 'pyramid_config',
53 53 "Set up a Pyramid environment with the specified config file.")
54 54
55 55 vcsgroup = parser.getgroup('vcs')
56 56 vcsgroup.addoption(
57 57 '--without-vcsserver', dest='with_vcsserver', action='store_false',
58 58 help="Do not start the VCSServer in a background process.")
59 59 vcsgroup.addoption(
60 60 '--with-vcsserver-http', dest='vcsserver_config_http',
61 61 help="Start the HTTP VCSServer with the specified config file.")
62 62 vcsgroup.addoption(
63 63 '--vcsserver-protocol', dest='vcsserver_protocol',
64 64 help="Start the VCSServer with HTTP protocol support.")
65 65 vcsgroup.addoption(
66 66 '--vcsserver-config-override', action='store', type=_parse_json,
67 67 default=None, dest='vcsserver_config_override', help=(
68 68 "Overrides the .ini file settings for the VCSServer. "
69 69 "Should be specified in JSON "
70 70 "format, e.g. '{\"section\": {\"parameter\": \"value\", ...}}'"
71 71 )
72 72 )
73 73 vcsgroup.addoption(
74 74 '--vcsserver-port', action='store', type=int,
75 75 default=None, help=(
76 76 "Allows to set the port of the vcsserver. Useful when testing "
77 77 "against an already running server and random ports cause "
78 78 "trouble."))
79 79 parser.addini(
80 80 'vcsserver_config_http',
81 81 "Start the HTTP VCSServer with the specified config file.")
82 82 parser.addini(
83 83 'vcsserver_protocol',
84 84 "Start the VCSServer with HTTP protocol support.")
85 85
86 86
87 87 @pytest.fixture(scope='session')
88 88 def vcsserver(request, vcsserver_port, vcsserver_factory):
89 89 """
90 90 Session scope VCSServer.
91 91
92 92 Tests wich need the VCSServer have to rely on this fixture in order
93 93 to ensure it will be running.
94 94
95 95 For specific needs, the fixture vcsserver_factory can be used. It allows to
96 96 adjust the configuration file for the test run.
97 97
98 98 Command line args:
99 99
100 100 --without-vcsserver: Allows to switch this fixture off. You have to
101 101 manually start the server.
102 102
103 103 --vcsserver-port: Will expect the VCSServer to listen on this port.
104 104 """
105 105
106 106 if not request.config.getoption('with_vcsserver'):
107 107 return None
108 108
109 109 return vcsserver_factory(
110 110 request, vcsserver_port=vcsserver_port)
111 111
112 112
113 113 @pytest.fixture(scope='session')
114 114 def vcsserver_factory(tmpdir_factory):
115 115 """
116 116 Use this if you need a running vcsserver with a special configuration.
117 117 """
118 118
119 119 def factory(request, overrides=(), vcsserver_port=None,
120 120 log_file=None):
121 121
122 122 if vcsserver_port is None:
123 123 vcsserver_port = get_available_port()
124 124
125 125 overrides = list(overrides)
126 126 overrides.append({'server:main': {'port': vcsserver_port}})
127 127
128 128 option_name = 'vcsserver_config_http'
129 129 override_option_name = 'vcsserver_config_override'
130 130 config_file = get_config(
131 131 request.config, option_name=option_name,
132 132 override_option_name=override_option_name, overrides=overrides,
133 133 basetemp=tmpdir_factory.getbasetemp().strpath,
134 134 prefix='test_vcs_')
135 135
136 136 server = RcVCSServer(config_file, log_file)
137 137 server.start()
138 138
139 139 @request.addfinalizer
140 140 def cleanup():
141 141 server.shutdown()
142 142
143 143 server.wait_until_ready()
144 144 return server
145 145
146 146 return factory
147 147
148 148
149 149 def is_cygwin():
150 150 return 'cygwin' in platform.system().lower()
151 151
152 152
153 153 def _use_log_level(config):
154 154 level = config.getoption('test_loglevel') or 'critical'
155 155 return level.upper()
156 156
157 157
158 158 @pytest.fixture(scope='session')
159 159 def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port):
160 160 option_name = 'pyramid_config'
161 161 log_level = _use_log_level(request.config)
162 162
163 163 overrides = [
164 164 {'server:main': {'port': rcserver_port}},
165 165 {'app:main': {
166 166 'vcs.server': 'localhost:%s' % vcsserver_port,
167 167 # johbo: We will always start the VCSServer on our own based on the
168 168 # fixtures of the test cases. For the test run it must always be
169 169 # off in the INI file.
170 170 'vcs.start_server': 'false',
171 171
172 172 'vcs.server.protocol': 'http',
173 173 'vcs.scm_app_implementation': 'http',
174 174 'vcs.hooks.protocol': 'http',
175 175 'vcs.hooks.host': '127.0.0.1',
176 176 }},
177 177
178 178 {'handler_console': {
179 'class ': 'StreamHandler',
180 'args ': '(sys.stderr,)',
179 'class': 'StreamHandler',
180 'args': '(sys.stderr,)',
181 181 'level': log_level,
182 182 }},
183 183
184 184 ]
185 185
186 186 filename = get_config(
187 187 request.config, option_name=option_name,
188 188 override_option_name='{}_override'.format(option_name),
189 189 overrides=overrides,
190 190 basetemp=tmpdir_factory.getbasetemp().strpath,
191 191 prefix='test_rce_')
192 192 return filename
193 193
194 194
195 195 @pytest.fixture(scope='session')
196 196 def ini_settings(ini_config):
197 197 ini_path = ini_config
198 198 return get_app_config(ini_path)
199 199
200 200
201 201 def get_available_port(min_port=40000, max_port=55555):
202 202 from rhodecode.lib.utils2 import get_available_port as _get_port
203 203 return _get_port(min_port, max_port)
204 204
205 205
206 206 @pytest.fixture(scope='session')
207 207 def rcserver_port(request):
208 208 port = get_available_port()
209 209 print('Using rhodecode port {}'.format(port))
210 210 return port
211 211
212 212
213 213 @pytest.fixture(scope='session')
214 214 def vcsserver_port(request):
215 215 port = request.config.getoption('--vcsserver-port')
216 216 if port is None:
217 217 port = get_available_port()
218 218 print('Using vcsserver port {}'.format(port))
219 219 return port
220 220
221 221
222 222 @pytest.fixture(scope='session')
223 223 def available_port_factory():
224 224 """
225 225 Returns a callable which returns free port numbers.
226 226 """
227 227 return get_available_port
228 228
229 229
230 230 @pytest.fixture()
231 231 def available_port(available_port_factory):
232 232 """
233 233 Gives you one free port for the current test.
234 234
235 235 Uses "available_port_factory" to retrieve the port.
236 236 """
237 237 return available_port_factory()
238 238
239 239
240 240 @pytest.fixture(scope='session')
241 241 def testini_factory(tmpdir_factory, ini_config):
242 242 """
243 243 Factory to create an INI file based on TestINI.
244 244
245 245 It will make sure to place the INI file in the correct directory.
246 246 """
247 247 basetemp = tmpdir_factory.getbasetemp().strpath
248 248 return TestIniFactory(basetemp, ini_config)
249 249
250 250
251 251 class TestIniFactory(object):
252 252
253 253 def __init__(self, basetemp, template_ini):
254 254 self._basetemp = basetemp
255 255 self._template_ini = template_ini
256 256
257 257 def __call__(self, ini_params, new_file_prefix='test'):
258 258 ini_file = TestINI(
259 259 self._template_ini, ini_params=ini_params,
260 260 new_file_prefix=new_file_prefix, dir=self._basetemp)
261 261 result = ini_file.create()
262 262 return result
263 263
264 264
265 265 def get_config(
266 266 config, option_name, override_option_name, overrides=None,
267 267 basetemp=None, prefix='test'):
268 268 """
269 269 Find a configuration file and apply overrides for the given `prefix`.
270 270 """
271 271 config_file = (
272 272 config.getoption(option_name) or config.getini(option_name))
273 273 if not config_file:
274 274 pytest.exit(
275 275 "Configuration error, could not extract {}.".format(option_name))
276 276
277 277 overrides = overrides or []
278 278 config_override = config.getoption(override_option_name)
279 279 if config_override:
280 280 overrides.append(config_override)
281 281 temp_ini_file = TestINI(
282 282 config_file, ini_params=overrides, new_file_prefix=prefix,
283 283 dir=basetemp)
284 284
285 285 return temp_ini_file.create()
@@ -1,201 +1,208 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import os
23 23 import time
24 24 import tempfile
25 25 import pytest
26 26 import subprocess
27 27 import logging
28 28 from urllib.request import urlopen
29 29 from urllib.error import URLError
30 30 import configparser
31 31
32 32
33 33 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS
34 34 from rhodecode.tests.utils import is_url_reachable
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 def get_port(pyramid_config):
40 40 config = configparser.ConfigParser()
41 41 config.read(pyramid_config)
42 42 return config.get('server:main', 'port')
43 43
44 44
45 45 def get_host_url(pyramid_config):
46 46 """Construct the host url using the port in the test configuration."""
47 47 port = get_port(pyramid_config)
48 48 return f'127.0.0.1:{port}'
49 49
50 50
51 51 def assert_no_running_instance(url):
52 52 if is_url_reachable(url):
53 53 print(f"Hint: Usually this means another instance of server "
54 54 f"is running in the background at {url}.")
55 55 pytest.fail(f"Port is not free at {url}, cannot start server at")
56 56
57 57
58 58 class ServerBase(object):
59 59 _args = []
60 60 log_file_name = 'NOT_DEFINED.log'
61 61 status_url_tmpl = 'http://{host}:{port}'
62 62
63 63 def __init__(self, config_file, log_file):
64 64 self.config_file = config_file
65 65 config = configparser.ConfigParser()
66 66 config.read(config_file)
67 67
68 68 self._config = {k: v for k, v in config['server:main'].items()}
69 69
70 70 self._args = []
71 71 self.log_file = log_file or os.path.join(
72 72 tempfile.gettempdir(), self.log_file_name)
73 73 self.process = None
74 74 self.server_out = None
75 75 log.info("Using the {} configuration:{}".format(
76 76 self.__class__.__name__, config_file))
77 77
78 78 if not os.path.isfile(config_file):
79 79 raise RuntimeError(f'Failed to get config at {config_file}')
80 80
81 81 @property
82 82 def command(self):
83 83 return ' '.join(self._args)
84 84
85 85 @property
86 def bind_addr(self):
87 return '{host}:{port}'.format(**self._config)
88
89 @property
86 90 def http_url(self):
87 91 template = 'http://{host}:{port}/'
88 92 return template.format(**self._config)
89 93
90 94 def host_url(self):
91 return 'http://' + get_host_url(self.config_file)
95 host = get_host_url(self.config_file)
96 return f'http://{host}'
92 97
93 98 def get_rc_log(self):
94 99 with open(self.log_file) as f:
95 100 return f.read()
96 101
97 102 def wait_until_ready(self, timeout=30):
98 103 host = self._config['host']
99 104 port = self._config['port']
100 105 status_url = self.status_url_tmpl.format(host=host, port=port)
101 106 start = time.time()
102 107
103 108 while time.time() - start < timeout:
104 109 try:
105 110 urlopen(status_url)
106 111 break
107 112 except URLError:
108 113 time.sleep(0.2)
109 114 else:
110 115 pytest.fail(
111 116 "Starting the {} failed or took more than {} "
112 117 "seconds. cmd: `{}`".format(
113 118 self.__class__.__name__, timeout, self.command))
114 119
115 120 log.info('Server of {} ready at url {}'.format(
116 121 self.__class__.__name__, status_url))
117 122
118 123 def shutdown(self):
119 124 self.process.kill()
120 125 self.server_out.flush()
121 126 self.server_out.close()
122 127
123 128 def get_log_file_with_port(self):
124 129 log_file = list(self.log_file.partition('.log'))
125 130 log_file.insert(1, get_port(self.config_file))
126 131 log_file = ''.join(log_file)
127 132 return log_file
128 133
129 134
130 135 class RcVCSServer(ServerBase):
131 136 """
132 137 Represents a running VCSServer instance.
133 138 """
134 139
135 140 log_file_name = 'rc-vcsserver.log'
136 141 status_url_tmpl = 'http://{host}:{port}/status'
137 142
138 143 def __init__(self, config_file, log_file=None):
139 144 super(RcVCSServer, self).__init__(config_file, log_file)
140 self._args = ['gunicorn', '--paste', self.config_file]
145 self._args = [
146 'gunicorn', '--bind', self.bind_addr,
147 '--paste', self.config_file]
141 148
142 149 def start(self):
143 150 env = os.environ.copy()
144 151
145 152 self.log_file = self.get_log_file_with_port()
146 153 self.server_out = open(self.log_file, 'w')
147 154
148 155 host_url = self.host_url()
149 156 assert_no_running_instance(host_url)
150 157
151 log.info('rhodecode-vcsserver start command: {}'.format(' '.join(self._args)))
152 log.info('rhodecode-vcsserver starting at: {}'.format(host_url))
153 log.info('rhodecode-vcsserver command: {}'.format(self.command))
154 log.info('rhodecode-vcsserver logfile: {}'.format(self.log_file))
158 print(f'rhodecode-vcsserver starting at: {host_url}')
159 print(f'rhodecode-vcsserver command: {self.command}')
160 print(f'rhodecode-vcsserver logfile: {self.log_file}')
155 161
156 162 self.process = subprocess.Popen(
157 163 self._args, bufsize=0, env=env,
158 164 stdout=self.server_out, stderr=self.server_out)
159 165
160 166
161 167 class RcWebServer(ServerBase):
162 168 """
163 169 Represents a running RCE web server used as a test fixture.
164 170 """
165 171
166 172 log_file_name = 'rc-web.log'
167 173 status_url_tmpl = 'http://{host}:{port}/_admin/ops/ping'
168 174
169 175 def __init__(self, config_file, log_file=None):
170 176 super(RcWebServer, self).__init__(config_file, log_file)
171 177 self._args = [
172 'gunicorn', '--worker-class', 'gevent', '--paste', config_file]
178 'gunicorn', '--bind', self.bind_addr, '--worker-class', 'gevent',
179 '--paste', self.config_file]
173 180
174 181 def start(self):
175 182 env = os.environ.copy()
176 183 env['RC_NO_TMP_PATH'] = '1'
177 184
178 185 self.log_file = self.get_log_file_with_port()
179 186 self.server_out = open(self.log_file, 'w')
180 187
181 188 host_url = self.host_url()
182 189 assert_no_running_instance(host_url)
183 190
184 log.info('rhodecode-web starting at: {}'.format(host_url))
185 log.info('rhodecode-web command: {}'.format(self.command))
186 log.info('rhodecode-web logfile: {}'.format(self.log_file))
191 print(f'rhodecode-web starting at: {host_url}')
192 print(f'rhodecode-web command: {self.command}')
193 print(f'rhodecode-web logfile: {self.log_file}')
187 194
188 195 self.process = subprocess.Popen(
189 196 self._args, bufsize=0, env=env,
190 197 stdout=self.server_out, stderr=self.server_out)
191 198
192 199 def repo_clone_url(self, repo_name, **kwargs):
193 200 params = {
194 201 'user': TEST_USER_ADMIN_LOGIN,
195 202 'passwd': TEST_USER_ADMIN_PASS,
196 203 'host': get_host_url(self.config_file),
197 204 'cloned_repo': repo_name,
198 205 }
199 206 params.update(**kwargs)
200 207 _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params
201 208 return _url
General Comments 0
You need to be logged in to leave comments. Login now