Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: cms/bin/test_server.py

Issue 29912588: Issue 7019 - [CMS] Refactor `test_server.py` (Closed)
Patch Set: Created Oct. 16, 2018, 11:42 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/test_page_outputs.py » ('j') | tests/test_page_outputs.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # This file is part of the Adblock Plus web scripts, 1 # This file is part of the Adblock Plus web scripts,
2 # Copyright (C) 2006-present eyeo GmbH 2 # Copyright (C) 2006-present eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details. 11 # GNU General Public License for more details.
12 # 12 #
13 # You should have received a copy of the GNU General Public License 13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15 15
16 from __future__ import print_function
17
18 import os
16 import mimetypes 19 import mimetypes
17 import os 20 from argparse import ArgumentParser
18 import sys
19 import argparse
20 21
21 import jinja2 22 import jinja2
22 23
24 from cms.converters import converters
23 from cms.utils import process_page 25 from cms.utils import process_page
24 from cms.sources import create_source 26 from cms.sources import create_source
25 from cms.converters import converters
26 27
27 source = None 28
28 address = None 29 class Parameters:
Vasily Kuznetsov 2018/10/16 13:18:23 As we discussed via IRC, let's make this whole thi
Tudor Avram 2018/10/18 13:44:05 Done.
29 port = None 30 source = None
31 host = None
32 port = None
33 base_url = None
34
35
36 MIME_FORMAT = '{}'
30 37
31 UNICODE_ENCODING = 'utf-8' 38 UNICODE_ENCODING = 'utf-8'
32 39
33 ERROR_TEMPLATE = ''' 40 ERROR_TEMPLATE = '''
34 <html> 41 <html>
35 <head> 42 <head>
36 <title>{{status}}</title> 43 <title>{{status}}</title>
37 </head> 44 </head>
38 <body> 45 <body>
39 <h1>{{status}}</h1> 46 <h1>{{status}}</h1>
40 {% set code = status.split()|first|int %} 47 {% set code = status.split()|first|int %}
41 {% if code == 404 %} 48 {% if code == 404 %}
42 <p>No page found for the address {{uri}}.</p> 49 <p>No page found for the address {{uri}}.</p>
43 {% elif code == 500 %} 50 {% elif code == 500 %}
44 <p>An error occurred while processing the request for {{uri}}:</p> 51 <p>An error occurred while processing the request for {{uri}}:</p>
45 <pre>{{error}}</pre> 52 <pre>{{error}}</pre>
46 {% endif %} 53 {% endif %}
47 </body> 54 </body>
48 </html>''' 55 </html>'''
49 56
50 # Initilize the mimetypes modules manually for consistent behavior, 57 # Initialize the mimetypes modules manually for consistent behavior,
51 # ignoring local files and Windows Registry. 58 # ignoring local files and Windows Registry.
52 mimetypes.init([]) 59 mimetypes.init([])
53 60
54 61
62 def get_data(path):
63 """Read the data corresponding to a page.
64
65 Parameters
66 ----------
67 path: str
68 The path to the page to get the data for.
69
70 Returns
71 -------
72 str/ bytes
73 The data corresponding to the page we're trying to open.
74
75 """
76 if Parameters.source.has_static(path):
77 return Parameters.source.read_static(path)
78
79 page, data = get_page(path)
80 if page and has_conflicts(page):
81 raise Exception('The requested page conflicts with another page')
82 return data
83
84
55 def get_page(path): 85 def get_page(path):
86 """Construct a page and return its contents.
87
88 Parameters
89 ----------
90 path: str
91 The path of the page we want to construct.
92
93 Returns
94 -------
95 (str, str)
96 With the following format:
97 <page_name, page_contents>
98
99 """
56 path = path.strip('/') 100 path = path.strip('/')
57 if path == '': 101 if path == '':
58 path = source.read_config().get('general', 'defaultlocale') 102 locale, page = Parameters.source.read_config().get(
59 if '/' in path: 103 'general', 'defaultlocale'), ''
104 elif '/' in path:
60 locale, page = path.split('/', 1) 105 locale, page = path.split('/', 1)
61 else: 106 else:
62 locale, page = path, '' 107 locale, page = path, ''
63 108
64 default_page = source.read_config().get('general', 'defaultpage') 109 default_page = Parameters.source.read_config().get('general',
65 alternative_page = '/'.join([page, default_page]).lstrip('/') 110 'defaultpage')
66 111 possible_pages = [page, '/'.join([page, default_page]).lstrip('/')]
67 for format in converters.iterkeys(): 112
68 for p in (page, alternative_page): 113 for page_format in converters.iterkeys():
69 if source.has_page(p, format): 114 for p in possible_pages:
70 site_url = 'http://{}:{}'.format(address, port) 115 if Parameters.source.has_page(p, page_format):
71 return (p, process_page(source, locale, p, format, site_url)) 116 return p, process_page(Parameters.source, locale, p,
72 if source.has_localizable_file(locale, page): 117 page_format, Parameters.base_url)
73 return (page, source.read_localizable_file(locale, page)) 118
74 119 if Parameters.source.has_localizable_file(locale, page):
75 return (None, None) 120 return page, Parameters.source.read_localizable_file(locale, page)
76 121
77 122 return None, None
78 def has_conflicting_pages(page): 123
79 pages = [p for p, _ in source.list_pages()] 124
80 pages.extend(source.list_localizable_files()) 125 def has_conflicts(page):
126 """Check if a page has conflicts.
127
128 A page has conflicts if there are other pages with the same name.
129 Parameters
130 ----------
131 page: str
132 The path of the page we're checking for conflicts.
133
134 Returns
135 -------
136 bool
137 True - if the page has conflicts
138 False - otherwise
139
140 """
141 pages = [p for p, _ in Parameters.source.list_pages()]
142 pages.extend(Parameters.source.list_localizable_files())
81 143
82 if pages.count(page) > 1: 144 if pages.count(page) > 1:
83 return True 145 return True
84 if any(p.startswith(page + '/') or page.startswith(p + '/') for p in pages): 146 if any(p.startswith(page + '/') or page.startswith(p + '/')
147 for p in pages):
85 return True 148 return True
86 return False 149 return False
87 150
88 151
89 def get_data(path): 152 def get_error_page(start_response, status, **kw):
90 if source.has_static(path): 153 """Create and display an error page.
91 return source.read_static(path) 154
92 155 Parameters
93 page, data = get_page(path) 156 ----------
94 if page and has_conflicting_pages(page): 157 start_response: function
95 raise Exception('The requested page conflicts with another page') 158 It will be called before constructing the error page, to setup
96 return data 159 things like the status of the response and the headers.
97 160 status: str
98 161 The status of the response we're sending the error page with.
99 def show_error(start_response, status, **kwargs): 162 Needs to have the following format: "<status_code> <status_message>"
163 kw: dict
164 Any additional arguments that will be passed onto the `stream` method
165 of a `jinja2 Template`.
166
167 Returns
168 -------
169 generator
170 of utf8 strings - fragments of the corresponding error HTML template.
171
172 """
100 env = jinja2.Environment(autoescape=True) 173 env = jinja2.Environment(autoescape=True)
101 template = env.from_string(ERROR_TEMPLATE) 174 page_template = env.from_string(ERROR_TEMPLATE)
102 mime = 'text/html; encoding=%s' % UNICODE_ENCODING 175 mime = 'text/html; encoding={}'.format(UNICODE_ENCODING)
176
103 start_response(status, [('Content-Type', mime)]) 177 start_response(status, [('Content-Type', mime)])
104 for fragment in template.stream(status=status, **kwargs): 178
179 for fragment in page_template.stream(status=status, **kw):
105 yield fragment.encode(UNICODE_ENCODING) 180 yield fragment.encode(UNICODE_ENCODING)
106 181
107 182
183 def set_parameters():
184 """Set the arguments required to run the script.
185
186 Performs the following actions:
187 1. Setup the script's argument parser
188 2. Read the arguments provided when running the script
189 3. Set the fields of the Parameters namespace
190
191 """
192 parser = ArgumentParser(description='CMS development server created to '
193 'test pages locally and on-the-fly')
194
195 parser.add_argument('path', default=os.curdir, nargs='?',
196 help='Path to the website we intend to run. If not '
197 'provided, defaults, to the current directory.')
198 parser.add_argument('--host', default='localhost',
199 help='Address of the host the server will listen on. '
200 'Defaults to "localhost".')
201 parser.add_argument('--port', default=5000, type=int,
202 help='TCP port the server will listen on. Default '
203 '5000.')
204
205 args = parser.parse_args()
206
207 Parameters.source = create_source(args.path)
208 Parameters.host = args.host
209 Parameters.port = args.port
210 Parameters.base_url = 'http://{0}:{1}'.format(args.host, args.port)
211
212
108 def handler(environ, start_response): 213 def handler(environ, start_response):
214 """Handle a request for a page.
215
216 Parameters
217 ----------
218 environ: dict
219 The environment under which the request si made.
220 start_response: function
221 Used to initiate a request response.
222
223 Returns
224 -------
225 [str]
226 With the response body.
227
228 """
109 path = environ.get('PATH_INFO') 229 path = environ.get('PATH_INFO')
110 230
111 data = get_data(path) 231 data = get_data(path)
232
112 if data is None: 233 if data is None:
113 return show_error(start_response, '404 Not Found', uri=path) 234 return get_error_page(start_response, '404 Not Found', uri=path)
114 235
115 mime = mimetypes.guess_type(path)[0] or 'text/html' 236 mime = mimetypes.guess_type(path)[0] or 'text/html'
116 237
117 if isinstance(data, unicode): 238 if isinstance(data, unicode):
118 data = data.encode(UNICODE_ENCODING) 239 data = data.encode(UNICODE_ENCODING)
119 mime = '%s; charset=%s' % (mime, UNICODE_ENCODING) 240 mime = '{0}; charset={1}'.format(mime, UNICODE_ENCODING)
120 241
121 start_response('200 OK', [('Content-Type', mime)]) 242 start_response('200 OK', [('Content-Type', mime)])
122 return [data] 243 return [data]
123 244
124 245
246 def make_werkzeug_server():
247 """Set up a server that uses `werkzeug`.
248
249 Returns
250 -------
251 function
252 Used to run the server
253
254 Raises
255 ------
256 ImportError
257 If the package `werkzeug` is not installed
258
259 """
260 from werkzeug.serving import run_simple
261
262 def run_func(*args, **kwargs):
263 # The werkzeug logger must be configured before the
264 # root logger. Also we must prevent it from propagating
265 # messages, otherwise messages are logged twice.
266 import logging
267 logger = logging.getLogger('werkzeug')
268 logger.propagate = False
269 logger.setLevel(logging.INFO)
270 logger.addHandler(logging.StreamHandler())
271
272 run_simple(threaded=True, *args, **kwargs)
273
274 return run_func
275
276
277 def make_builtins_server():
278 """Configure a server that only uses builtin packages.
279
280 Returns
281 -------
282 function
283 Used to run the server.
284
285 """
286 from SocketServer import ThreadingMixIn
287 from wsgiref.simple_server import WSGIServer, make_server
288
289 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
290 daemon_threads = True
291
292 def run(host, port, app, **kwargs):
293 def wrapper(environ, start_response):
294 try:
295 return app(environ, start_response)
296 except Exception as e:
297 return get_error_page(start_response,
298 '500 Internal Server Error',
299 uri=environ.get('PATH_INFO'), error=e)
300
301 server = make_server(host, port, wrapper,
302 ThreadedWSGIServer)
303 print(' * Running on {0}:{1}'.format(*server.server_address))
304 server.serve_forever()
305
306 return run
307
308
309 def main():
310 set_parameters()
311 try:
312 run = make_werkzeug_server()
313 except ImportError:
314 run = make_builtins_server()
315 run(Parameters.host, Parameters.port, handler, use_reloader=True,
316 use_debugger=True)
317
318
125 if __name__ == '__main__': 319 if __name__ == '__main__':
126 320 main()
127 parser = argparse.ArgumentParser(description='CMS development server created to test pages locally and on-the-fly')
128 parser.add_argument('path', nargs='?', default=os.curdir)
129 parser.add_argument('-a', '--address', default='localhost', help='Address of the interface the server will listen on')
130 parser.add_argument('-p', '--port', type=int, default=5000, help='TCP port t he server will listen on')
131 args = parser.parse_args()
132
133 source = create_source(args.path)
134 address = args.address
135 port = args.port
136
137 try:
138 from werkzeug.serving import ThreadedWSGIServer, run_simple
139
140 # see https://github.com/mitsuhiko/werkzeug/pull/770
141 ThreadedWSGIServer.daemon_threads = True
142
143 def run(*args, **kwargs):
144 # The werkzeug logger must be configured before the
145 # root logger. Also we must prevent it from propagating
146 # messages, otherwise messages are logged twice.
147 import logging
148 logger = logging.getLogger('werkzeug')
149 logger.propagate = False
150 logger.setLevel(logging.INFO)
151 logger.addHandler(logging.StreamHandler())
152
153 run_simple(threaded=True, *args, **kwargs)
154 except ImportError:
155 from SocketServer import ThreadingMixIn
156 from wsgiref.simple_server import WSGIServer, make_server
157
158 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
159 daemon_threads = True
160
161 def run(host, port, app, **kwargs):
162 def wrapper(environ, start_response):
163 try:
164 return app(environ, start_response)
165 except Exception as e:
166 return show_error(start_response, '500 Internal Server Error ',
167 uri=environ.get('PATH_INFO'), error=e)
168
169 server = make_server(host, port, wrapper, ThreadedWSGIServer)
170 print ' * Running on http://%s:%i/' % server.server_address
171 server.serve_forever()
172
173 run(address, port, handler, use_reloader=True, use_debugger=True)
OLDNEW
« no previous file with comments | « no previous file | tests/test_page_outputs.py » ('j') | tests/test_page_outputs.py » ('J')

Powered by Google App Engine
This is Rietveld