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