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: Addressed commments from Patch Set #1. Objectified server handler. Added test cases. Created Oct. 18, 2018, 1:41 p.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/conftest.py » ('j') | no next file with comments »
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 source = None
28 address = None
29 port = None
30 27
31 UNICODE_ENCODING = 'utf-8' 28 UNICODE_ENCODING = 'utf-8'
32 29
33 ERROR_TEMPLATE = ''' 30 ERROR_TEMPLATE = '''
34 <html> 31 <html>
35 <head> 32 <head>
36 <title>{{status}}</title> 33 <title>{{status}}</title>
37 </head> 34 </head>
38 <body> 35 <body>
39 <h1>{{status}}</h1> 36 <h1>{{status}}</h1>
40 {% set code = status.split()|first|int %} 37 {% set code = status.split()|first|int %}
41 {% if code == 404 %} 38 {% if code == 404 %}
42 <p>No page found for the address {{uri}}.</p> 39 <p>No page found for the address {{uri}}.</p>
43 {% elif code == 500 %} 40 {% elif code == 500 %}
44 <p>An error occurred while processing the request for {{uri}}:</p> 41 <p>An error occurred while processing the request for {{uri}}:</p>
45 <pre>{{error}}</pre> 42 <pre>{{error}}</pre>
46 {% endif %} 43 {% endif %}
47 </body> 44 </body>
48 </html>''' 45 </html>'''
49 46
50 # Initilize the mimetypes modules manually for consistent behavior, 47 # Initialize the mimetypes modules manually for consistent behavior,
51 # ignoring local files and Windows Registry. 48 # ignoring local files and Windows Registry.
52 mimetypes.init([]) 49 mimetypes.init([])
53 50
54 51
55 def get_page(path): 52 class DynamicServerHandler:
56 path = path.strip('/') 53 """General-purpose WSGI server handler that generates pages on request.
57 if path == '': 54
58 path = source.read_config().get('general', 'defaultlocale') 55 Parameters
59 if '/' in path: 56 ----------
60 locale, page = path.split('/', 1) 57 host: str
61 else: 58 The host where the server will run.
62 locale, page = path, '' 59 port: int
63 60 The TCP port the server will run on.
64 default_page = source.read_config().get('general', 'defaultpage') 61 source_dir: str
65 alternative_page = '/'.join([page, default_page]).lstrip('/') 62 The path to the website contents.
66 63
67 for format in converters.iterkeys(): 64 """
68 for p in (page, alternative_page): 65
69 if source.has_page(p, format): 66 def __init__(self, host, port, source_dir):
70 site_url = 'http://{}:{}'.format(address, port) 67 self.host = host
71 return (p, process_page(source, locale, p, format, site_url)) 68 self.port = port
72 if source.has_localizable_file(locale, page): 69 self.source = create_source(source_dir)
73 return (page, source.read_localizable_file(locale, page)) 70 self.full_url = 'http://{0}:{1}'.format(host, port)
74 71
75 return (None, None) 72 def _get_data(self, path):
76 73 """Read the data corresponding to a website path.
77 74
78 def has_conflicting_pages(page): 75 Parameters
79 pages = [p for p, _ in source.list_pages()] 76 ----------
80 pages.extend(source.list_localizable_files()) 77 path: str
81 78 The path to the page to get the data for.
82 if pages.count(page) > 1: 79
83 return True 80 Returns
84 if any(p.startswith(page + '/') or page.startswith(p + '/') for p in pages): 81 -------
85 return True 82 str/ bytes
86 return False 83 The data corresponding to the path we're trying to access.
87 84
88 85 """
89 def get_data(path): 86 if self.source.has_static(path):
90 if source.has_static(path): 87 return self.source.read_static(path)
91 return source.read_static(path) 88
92 89 page, data = self._get_page(path)
93 page, data = get_page(path) 90
94 if page and has_conflicting_pages(page): 91 if page and self._has_conflicts(page):
95 raise Exception('The requested page conflicts with another page') 92 raise Exception('The requested page conflicts with another page.')
96 return data 93
97 94 return data
98 95
99 def show_error(start_response, status, **kwargs): 96 def _get_page(self, path):
100 env = jinja2.Environment(autoescape=True) 97 """Construct a page and return its contents.
101 template = env.from_string(ERROR_TEMPLATE) 98
102 mime = 'text/html; encoding=%s' % UNICODE_ENCODING 99 Parameters
103 start_response(status, [('Content-Type', mime)]) 100 ----------
104 for fragment in template.stream(status=status, **kwargs): 101 path: str
105 yield fragment.encode(UNICODE_ENCODING) 102 The path of the page we want to construct.
106 103
107 104 Returns
108 def handler(environ, start_response): 105 -------
109 path = environ.get('PATH_INFO') 106 (str, str)
110 107 With the following format:
111 data = get_data(path) 108 <page_name, page_contents>
112 if data is None: 109
113 return show_error(start_response, '404 Not Found', uri=path) 110 """
114 111 path = path.strip('/')
115 mime = mimetypes.guess_type(path)[0] or 'text/html' 112 if path == '':
116 113 locale, page = self.source.read_config().get('general',
117 if isinstance(data, unicode): 114 'defaultlocale'), ''
118 data = data.encode(UNICODE_ENCODING) 115 elif '/' in path:
119 mime = '%s; charset=%s' % (mime, UNICODE_ENCODING) 116 locale, page = path.split('/', 1)
120 117 else:
121 start_response('200 OK', [('Content-Type', mime)]) 118 locale, page = path, ''
122 return [data] 119
120 default_page = self.source.read_config().get('general', 'defaultpage')
121 possible_pages = [page, '/'.join([page, default_page]).lstrip('/')]
122
123 for page_format in converters.iterkeys():
124 for p in possible_pages:
125 if self.source.has_page(p, page_format):
126 return p, process_page(self.source, locale, p, page_format,
127 self.full_url)
128
129 if self.source.has_localizable_file(locale, page):
130 return page, self.source.read_localizable_file(locale, page)
131
132 return None, None
133
134 def _has_conflicts(self, page):
135 """Check if a page has conflicts.
136
137 A page has conflicts if there are other pages with the same name.
138 Parameters
139 ----------
140 page: str
141 The path of the page we're checking for conflicts.
142
143 Returns
144 -------
145 bool
146 True - if the page has conflicts
147 False - otherwise
148
149 """
150 pages = [p for p, _ in self.source.list_pages()]
151 pages.extend(self.source.list_localizable_files())
152
153 if pages.count(page) > 1:
154 return True
155 if any(p.startswith(page + '/') or page.startswith(p + '/') for p in
156 pages):
157 return True
158 return False
159
160 def get_error_page(self, start_response, status, **kw):
161 """Create and display an error page.
162
163 Parameters
164 ----------
165 start_response: function
166 It will be called before constructing the error page, to setup
167 things like the status of the response and the headers.
168 status: str
169 The status of the response we're sending the error page with.
170 Needs to have the following format: "<status_code>
171 <status_message>"
172 kw: dict
173 Any additional arguments that will be passed onto the `stream`
174 method
175 of a `jinja2 Template`.
176
177 Returns
178 -------
179 generator
180 of utf8 strings - fragments of the corresponding error HTML
181 template.
182
183 """
184 env = jinja2.Environment(autoescape=True)
185 page_template = env.from_string(ERROR_TEMPLATE)
186 mime = 'text/html; encoding={}'.format(UNICODE_ENCODING)
187
188 start_response(status, [('Content-Type', mime)])
189
190 for fragment in page_template.stream(status=status, **kw):
191 yield fragment.encode(UNICODE_ENCODING)
192
193 def __call__(self, environ, start_response):
194 """Execute the handler, according with the WSGI standards.
195
196 Parameters
197 ---------
198 environ: dict
199 The environment under which the page is requested.
200 The requested page must be under the `PATH_INFO` key.
201 start_response: function
202 Used to initiate a response. Must take two arguments, in this
203 order:
204 - Response status, in the format "<code> <message>".
205 - Response headers, as a list of tuples.
206
207 Returns
208 -------
209 list
210 With the data for a specific page.
211
212 """
213 path = environ.get('PATH_INFO')
214
215 data = self._get_data(path)
216
217 if data is None:
218 return self.get_error_page(start_response, '404 Not Found',
219 uri=path)
220
221 mime = mimetypes.guess_type(path)[0] or 'text/html'
222
223 if isinstance(data, unicode):
224 data = data.encode(UNICODE_ENCODING)
225 mime = '{0}; charset={1}'.format(mime, UNICODE_ENCODING)
226
227 start_response('200 OK', [('Content-Type', mime)])
228 return [data]
229
230
231 def get_handler():
232 """Set the arguments required to run the script.
233
234 Returns
235 -------
236 TestServerHandler
237 Initialised with the script parameters.
238
239 """
240 parser = ArgumentParser(description='CMS development server created to '
241 'test pages locally and on-the-fly')
242
243 parser.add_argument('path', default=os.curdir, nargs='?',
244 help='Path to the website we intend to run. If not '
245 'provided, defaults, to the current directory.')
246 parser.add_argument('--host', default='localhost',
247 help='Address of the host the server will listen on. '
248 'Defaults to "localhost".')
249 parser.add_argument('--port', default=5000, type=int,
250 help='TCP port the server will listen on. Default '
251 '5000.')
252
253 args = parser.parse_args()
254
255 return DynamicServerHandler(args.host, args.port, args.path)
256
257
258 def run_werkzeug_server(handler, **kw):
259 """Set up a server that uses `werkzeug`.
260
261 Parameters
262 ----------
263 handler: DynamicServerHandler
264 Defines the parameters and methods required to handle requests.
265
266 Raises
267 ------
268 ImportError
269 If the package `werkzeug` is not installed
270
271 """
272 from werkzeug.serving import run_simple
273
274 def run(*args, **kwargs):
275 # The werkzeug logger must be configured before the
276 # root logger. Also we must prevent it from propagating
277 # messages, otherwise messages are logged twice.
278 import logging
279 logger = logging.getLogger('werkzeug')
280 logger.propagate = False
281 logger.setLevel(logging.INFO)
282 logger.addHandler(logging.StreamHandler())
283
284 run_simple(threaded=True, *args, **kwargs)
285
286 run(handler.host, handler.port, handler, **kw)
287
288
289 def run_builtins_server(handler, **kw):
290 """Configure a server that only uses builtin packages.
291
292 Parameters
293 ----------
294 handler: DynamicServerHandler
295 Defines the parameters and methods required to handle requests.
296
297 """
298 from SocketServer import ThreadingMixIn
299 from wsgiref.simple_server import WSGIServer, make_server
300
301 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
302 daemon_threads = True
303
304 def run(host, port, app, **kwargs):
305 def wrapper(environ, start_response):
306 try:
307 return app(environ, start_response)
308 except Exception as e:
309 return handler.get_error_page(
310 start_response, '500 Internal Server Error',
311 uri=environ.get('PATH_INFO'), error=e,
312 )
313
314 server = make_server(host, port, wrapper, ThreadedWSGIServer)
315 print(' * Running on {0}:{1}'.format(*server.server_address))
316 server.serve_forever()
317
318 run(handler.host, handler.port, handler, **kw)
319
320
321 def main():
322 handler = get_handler()
323
324 try:
325 run_werkzeug_server(handler, use_reloader=True, use_debugger=True)
326 except ImportError:
327 run_builtins_server(handler)
123 328
124 329
125 if __name__ == '__main__': 330 if __name__ == '__main__':
126 331 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/conftest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld