Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(494)

Side by Side Diff: Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/pipes.py

Issue 1154373005: Introduce WPTServe for running W3C Blink Layout tests (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Add executable bit to pass permchecks. Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 from cgi import escape
2 import gzip as gzip_module
3 import re
4 import time
5 import types
6 import uuid
7 from cStringIO import StringIO
8
9
10 def resolve_content(response):
11 rv = "".join(item for item in response.iter_content())
12 if type(rv) == unicode:
13 rv = rv.encode(response.encoding)
14 return rv
15
16
17 class Pipeline(object):
18 pipes = {}
19
20 def __init__(self, pipe_string):
21 self.pipe_functions = self.parse(pipe_string)
22
23 def parse(self, pipe_string):
24 functions = []
25 for item in PipeTokenizer().tokenize(pipe_string):
26 if not item:
27 break
28 if item[0] == "function":
29 functions.append((self.pipes[item[1]], []))
30 elif item[0] == "argument":
31 functions[-1][1].append(item[1])
32 return functions
33
34 def __call__(self, request, response):
35 for func, args in self.pipe_functions:
36 response = func(request, response, *args)
37 return response
38
39
40 class PipeTokenizer(object):
41 def __init__(self):
42 #This whole class can likely be replaced by some regexps
43 self.state = None
44
45 def tokenize(self, string):
46 self.string = string
47 self.state = self.func_name_state
48 self._index = 0
49 while self.state:
50 yield self.state()
51 yield None
52
53 def get_char(self):
54 if self._index >= len(self.string):
55 return None
56 rv = self.string[self._index]
57 self._index += 1
58 return rv
59
60 def func_name_state(self):
61 rv = ""
62 while True:
63 char = self.get_char()
64 if char is None:
65 self.state = None
66 if rv:
67 return ("function", rv)
68 else:
69 return None
70 elif char == "(":
71 self.state = self.argument_state
72 return ("function", rv)
73 elif char == "|":
74 if rv:
75 return ("function", rv)
76 else:
77 rv += char
78
79 def argument_state(self):
80 rv = ""
81 while True:
82 char = self.get_char()
83 if char is None:
84 self.state = None
85 return ("argument", rv)
86 elif char == "\\":
87 rv += self.get_escape()
88 if rv is None:
89 #This should perhaps be an error instead
90 return ("argument", rv)
91 elif char == ",":
92 return ("argument", rv)
93 elif char == ")":
94 self.state = self.func_name_state
95 return ("argument", rv)
96 else:
97 rv += char
98
99 def get_escape(self):
100 char = self.get_char()
101 escapes = {"n": "\n",
102 "r": "\r",
103 "t": "\t"}
104 return escapes.get(char, char)
105
106
107 class pipe(object):
108 def __init__(self, *arg_converters):
109 self.arg_converters = arg_converters
110 self.max_args = len(self.arg_converters)
111 self.min_args = 0
112 opt_seen = False
113 for item in self.arg_converters:
114 if not opt_seen:
115 if isinstance(item, opt):
116 opt_seen = True
117 else:
118 self.min_args += 1
119 else:
120 if not isinstance(item, opt):
121 raise ValueError("Non-optional argument cannot follow option al argument")
122
123 def __call__(self, f):
124 def inner(request, response, *args):
125 if not (self.min_args <= len(args) <= self.max_args):
126 raise ValueError("Expected between %d and %d args, got %d" %
127 (self.min_args, self.max_args, len(args)))
128 arg_values = tuple(f(x) for f, x in zip(self.arg_converters, args))
129 return f(request, response, *arg_values)
130 Pipeline.pipes[f.__name__] = inner
131 #We actually want the undecorated function in the main namespace
132 return f
133
134
135 class opt(object):
136 def __init__(self, f):
137 self.f = f
138
139 def __call__(self, arg):
140 return self.f(arg)
141
142
143 def nullable(func):
144 def inner(arg):
145 if arg.lower() == "null":
146 return None
147 else:
148 return func(arg)
149 return inner
150
151
152 def boolean(arg):
153 if arg.lower() in ("true", "1"):
154 return True
155 elif arg.lower() in ("false", "0"):
156 return False
157 raise ValueError
158
159
160 @pipe(int)
161 def status(request, response, code):
162 """Alter the status code.
163
164 :param code: Status code to use for the response."""
165 response.status = code
166 return response
167
168
169 @pipe(str, str, opt(boolean))
170 def header(request, response, name, value, append=False):
171 """Set a HTTP header.
172
173 Replaces any existing HTTP header of the same name unless
174 append is set, in which case the header is appended without
175 replacement.
176
177 :param name: Name of the header to set.
178 :param value: Value to use for the header.
179 :param append: True if existing headers should not be replaced
180 """
181 if not append:
182 response.headers.set(name, value)
183 else:
184 response.headers.append(name, value)
185 return response
186
187
188 @pipe(str)
189 def trickle(request, response, delays):
190 """Send the response in parts, with time delays.
191
192 :param delays: A string of delays and amounts, in bytes, of the
193 response to send. Each component is separated by
194 a colon. Amounts in bytes are plain integers, whilst
195 delays are floats prefixed with a single d e.g.
196 d1:100:d2
197 Would cause a 1 second delay, would then send 100 bytes
198 of the file, and then cause a 2 second delay, before sending
199 the remainder of the file.
200
201 If the last token is of the form rN, instead of sending the
202 remainder of the file, the previous N instructions will be
203 repeated until the whole file has been sent e.g.
204 d1:100:d2:r2
205 Causes a delay of 1s, then 100 bytes to be sent, then a 2s de lay
206 and then a further 100 bytes followed by a two second delay
207 until the response has been fully sent.
208 """
209 def parse_delays():
210 parts = delays.split(":")
211 rv = []
212 for item in parts:
213 if item.startswith("d"):
214 item_type = "delay"
215 item = item[1:]
216 value = float(item)
217 elif item.startswith("r"):
218 item_type = "repeat"
219 value = int(item[1:])
220 if not value % 2 == 0:
221 raise ValueError
222 else:
223 item_type = "bytes"
224 value = int(item)
225 if len(rv) and rv[-1][0] == item_type:
226 rv[-1][1] += value
227 else:
228 rv.append((item_type, value))
229 return rv
230
231 delays = parse_delays()
232 if not delays:
233 return response
234 content = resolve_content(response)
235 modified_content = []
236 offset = [0]
237
238 def sleep(seconds):
239 def inner():
240 time.sleep(seconds)
241 return ""
242 return inner
243
244 def add_content(delays, repeat=False):
245 for i, (item_type, value) in enumerate(delays):
246 if item_type == "bytes":
247 modified_content.append(content[offset[0]:offset[0] + value])
248 offset[0] += value
249 elif item_type == "delay":
250 modified_content.append(sleep(value))
251 elif item_type == "repeat":
252 assert i == len(delays) - 1
253 while offset[0] < len(content):
254 add_content(delays[-(value + 1):-1], True)
255
256 if not repeat and offset[0] < len(content):
257 modified_content.append(content[offset[0]:])
258
259 add_content(delays)
260
261 response.content = modified_content
262 return response
263
264
265 @pipe(nullable(int), opt(nullable(int)))
266 def slice(request, response, start, end=None):
267 """Send a byte range of the response body
268
269 :param start: The starting offset. Follows python semantics including
270 negative numbers.
271
272 :param end: The ending offset, again with python semantics and None
273 (spelled "null" in a query string) to indicate the end of
274 the file.
275 """
276 content = resolve_content(response)
277 response.content = content[start:end]
278 return response
279
280
281 class ReplacementTokenizer(object):
282 def ident(scanner, token):
283 return ("ident", token)
284
285 def index(scanner, token):
286 token = token[1:-1]
287 try:
288 token = int(token)
289 except ValueError:
290 token = unicode(token, "utf8")
291 return ("index", token)
292
293 def var(scanner, token):
294 token = token[:-1]
295 return ("var", token)
296
297 def tokenize(self, string):
298 return self.scanner.scan(string)[0]
299
300 scanner = re.Scanner([(r"\$\w+:", var),
301 (r"\$?\w+(?:\(\))?", ident),
302 (r"\[[^\]]*\]", index)])
303
304
305 class FirstWrapper(object):
306 def __init__(self, params):
307 self.params = params
308
309 def __getitem__(self, key):
310 try:
311 return self.params.first(key)
312 except KeyError:
313 return ""
314
315
316 @pipe()
317 def sub(request, response):
318 """Substitute environment information about the server and request into the script.
319
320 The format is a very limited template language. Substitutions are
321 enclosed by {{ and }}. There are several avaliable substitutions:
322
323 host
324 A simple string value and represents the primary host from which the
325 tests are being run.
326 domains
327 A dictionary of available domains indexed by subdomain name.
328 ports
329 A dictionary of lists of ports indexed by protocol.
330 location
331 A dictionary of parts of the request URL. Valid keys are
332 'server, 'scheme', 'host', 'hostname', 'port', 'path' and 'query'.
333 'server' is scheme://host:port, 'host' is hostname:port, and query
334 includes the leading '?', but other delimiters are omitted.
335 headers
336 A dictionary of HTTP headers in the request.
337 GET
338 A dictionary of query parameters supplied with the request.
339 uuid()
340 A pesudo-random UUID suitable for usage with stash
341
342 So for example in a setup running on localhost with a www
343 subdomain and a http server on ports 80 and 81::
344
345 {{host}} => localhost
346 {{domains[www]}} => www.localhost
347 {{ports[http][1]}} => 81
348
349
350 It is also possible to assign a value to a variable name, which must start w ith
351 the $ character, using the ":" syntax e.g.
352
353 {{$id:uuid()}
354
355 Later substitutions in the same file may then refer to the variable
356 by name e.g.
357
358 {{$id}}
359 """
360 content = resolve_content(response)
361
362 new_content = template(request, content)
363
364 response.content = new_content
365 return response
366
367 def template(request, content):
368 #TODO: There basically isn't any error handling here
369 tokenizer = ReplacementTokenizer()
370
371 variables = {}
372
373 def config_replacement(match):
374 content, = match.groups()
375
376 tokens = tokenizer.tokenize(content)
377
378 if tokens[0][0] == "var":
379 variable = tokens[0][1]
380 tokens = tokens[1:]
381 else:
382 variable = None
383
384 assert tokens[0][0] == "ident" and all(item[0] == "index" for item in to kens[1:]), tokens
385
386 field = tokens[0][1]
387
388 if field in variables:
389 value = variables[field]
390 elif field == "headers":
391 value = request.headers
392 elif field == "GET":
393 value = FirstWrapper(request.GET)
394 elif field in request.server.config:
395 value = request.server.config[tokens[0][1]]
396 elif field == "location":
397 value = {"server": "%s://%s:%s" % (request.url_parts.scheme,
398 request.url_parts.hostname,
399 request.url_parts.port),
400 "scheme": request.url_parts.scheme,
401 "host": "%s:%s" % (request.url_parts.hostname,
402 request.url_parts.port),
403 "hostname": request.url_parts.hostname,
404 "port": request.url_parts.port,
405 "path": request.url_parts.path,
406 "query": "?%s" % request.url_parts.query}
407 elif field == "uuid()":
408 value = str(uuid.uuid4())
409 else:
410 raise Exception("Undefined template variable %s" % field)
411
412 for item in tokens[1:]:
413 value = value[item[1]]
414
415 assert isinstance(value, (int,) + types.StringTypes), tokens
416
417 if variable is not None:
418 variables[variable] = value
419
420 #Should possibly support escaping for other contexts e.g. script
421 #TODO: read the encoding of the response
422 return escape(unicode(value)).encode("utf-8")
423
424 template_regexp = re.compile(r"{{([^}]*)}}")
425 new_content, count = template_regexp.subn(config_replacement, content)
426
427 return new_content
428
429 @pipe()
430 def gzip(request, response):
431 """This pipe gzip-encodes response data.
432
433 It sets (or overwrites) these HTTP headers:
434 Content-Encoding is set to gzip
435 Content-Length is set to the length of the compressed content
436 """
437 content = resolve_content(response)
438 response.headers.set("Content-Encoding", "gzip")
439
440 out = StringIO()
441 with gzip_module.GzipFile(fileobj=out, mode="w") as f:
442 f.write(content)
443 response.content = out.getvalue()
444
445 response.headers.set("Content-Length", len(response.content))
446
447 return response
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698