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

Side by Side Diff: build/android/pylib/chrome_test_server_spawner.py

Issue 10885005: Upstream chrome_test_server_spawner.py for Android. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 3 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 | Annotate | Revision Log
« no previous file with comments | « build/android/pylib/base_test_runner.py ('k') | build/android/pylib/constants.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 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """A "Test Server Spawner" that handles killing/stopping per-test test servers. 5 """A "Test Server Spawner" that handles killing/stopping per-test test servers.
6 6
7 It's used to accept requests from the device to spawn and kill instances of the 7 It's used to accept requests from the device to spawn and kill instances of the
8 chrome test server on the host. 8 chrome test server on the host.
9 """ 9 """
10 10
11 import BaseHTTPServer 11 import BaseHTTPServer
12 import logging 12 import logging
13 import json
bulach 2012/08/29 09:22:54 nit: before logging
Philippe 2012/08/29 11:29:05 Done.
13 import os 14 import os
15 import select
16 import struct
17 import subprocess
14 import sys 18 import sys
15 import threading 19 import threading
16 import time 20 import time
17 import urlparse 21 import urlparse
18 22
19 # Path that are needed to import testserver 23 import constants
20 cr_src = os.path.join(os.path.abspath(os.path.dirname(__file__)), 24 from forwarder import Forwarder
21 '..', '..', '..') 25 import ports
22 sys.path.append(os.path.join(cr_src, 'third_party')) 26
bulach 2012/08/29 09:22:54 nit: need another \n
Philippe 2012/08/29 11:29:05 Done.
23 sys.path.append(os.path.join(cr_src, 'third_party', 'tlslite')) 27 # Path that are needed to import necessary modules when running testserver.py.
24 sys.path.append(os.path.join(cr_src, 'third_party', 'pyftpdlib', 'src')) 28 os.environ['PYTHONPATH'] += ':%s:%s:%s:%s' % (
25 sys.path.append(os.path.join(cr_src, 'net', 'tools', 'testserver')) 29 os.path.join(constants.CHROME_DIR, 'third_party'),
26 import testserver 30 os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'),
27 31 os.path.join(constants.CHROME_DIR, 'third_party', 'pyftpdlib', 'src'),
28 _test_servers = [] 32 os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver'))
33
Philippe 2012/08/29 11:29:05 Do we need another \n here? I'm adding it to be co
34 SERVER_TYPES = {
35 'http': '',
bulach 2012/08/29 09:22:54 nit: 35-39 needs indent by 4
Philippe 2012/08/29 11:29:05 Done.
36 'ftp': '-f',
37 'sync': '--sync',
38 'tcpecho': '--tcp-echo',
39 'udpecho': '--udp-echo',
40 }
41
bulach 2012/08/29 09:22:54 nit: need another \n
Philippe 2012/08/29 11:29:05 Done.
42 # The timeout (in seconds) of starting up the Python test server.
43 TEST_SERVER_STARTUP_TIMEOUT = 10
44
45
46 def CheckPortStatus(port, expected_status):
bulach 2012/08/29 09:22:54 nit: it seems that this function, and most of the
Philippe 2012/08/29 11:29:05 Sounds good. Done.
47 """Returns True if port has expected_status.
48
49 Args:
50 port: the port number.
51 expected_status: boolean of expected status.
52 Returns:
53 Returns True if the status is expected. Otherwise returns False.
54 """
55 for timeout in range(1, 5):
56 if ports.IsHostPortUsed(port) == expected_status:
57 return True
58 time.sleep(timeout)
59 return False
60
61
62 def GetServerTypeCommandLine(server_type_string):
63 """Returns the command-line by the given server type.
64
65 This function translates the server type string into the appropriate
66 command-line argument by following the option definitions in testserver.py.
bulach 2012/08/29 09:22:54 nit: need "Args:" and "Returns:".. also, it'd be n
Philippe 2012/08/29 11:29:05 Done.
67 """
68 if server_type_string not in SERVER_TYPES:
69 raise NotImplementedError('Unknown server type: %s' % server_type)
bulach 2012/08/29 09:22:54 nit: server_type is undefined, needs to be "server
Philippe 2012/08/29 11:29:05 Done.
70 if server_type_string == 'udpecho':
71 raise Exception('Please do not run UDP echo tests because we do not have '
72 'a UDP forwarder tool.')
73 return SERVER_TYPES[server_type_string]
74
75
76 class TestServerThread(threading.Thread):
77 """The class to run a test server in a separate process in a thread.
bulach 2012/08/29 09:22:54 nit: needs to be one line, probably something like
Philippe 2012/08/29 11:29:05 Done.
78 """
79
80 def __init__(self, ready_event, arguments, adb, tool, build_type):
81 """Initialize TestServerThread with the following argument.
82
83 Args:
84 ready_event: event which will be set when the test server is ready.
85 arguments: dictionary of arguments to run the test server.
86 adb: instance of AndroidCommands.
87 tool: instance of runtime error detection tool.
88 build_type: 'Release' or 'Debug'.
89 """
90 threading.Thread.__init__(self)
91 self.wait_event = threading.Event()
92 self.stop_flag = False
93 self.ready_event = ready_event
94 self.ready_event.clear()
95 self.arguments = arguments
96 self.adb = adb
97 self.tool = tool
98 self.test_server_process = None
99 self.is_ready = False
100 self.host_port = self.arguments['port']
101 assert isinstance(self.host_port, int)
102 self._test_server_forwarder = None
103 # The forwarder device port now is dynamically allocated.
104 self.forwarder_device_port = 0;
bulach 2012/08/29 09:22:54 nit: remove ;
Philippe 2012/08/29 11:29:05 Done.
105 # Anonymous pipe in order to get port info from test server.
106 self.pipe_in = None
107 self.pipe_out = None
108 self.command_line = []
109 self.build_type = build_type
110
111 def WaitToStartAndGetPortFromTestServer(self):
112 """Waits for the Python test server to start and get port it is using. The
bulach 2012/08/29 09:22:54 nit: needs a one line docstring.
Philippe 2012/08/29 11:29:05 Done.
113 port information is passed by the Python test server with a pipe given by
114 self.pipe_out.
115
116 Return:
bulach 2012/08/29 09:22:54 nit: Returns:
Philippe 2012/08/29 11:29:05 Done.
117 True if successfully gets port used by the Python test server.
118 """
119 assert self.host_port == 0 and self.pipe_out and self.pipe_in
120 (in_fds, out_fds, err_fds) = select.select([self.pipe_in,],[],[],
bulach 2012/08/29 09:22:54 nit: out_fds and err_fds seem unused, maybe replac
Philippe 2012/08/29 11:29:05 Done.
121 TEST_SERVER_STARTUP_TIMEOUT)
122 if len(in_fds) == 0:
123 logging.error('Failed to wait to the Python test server to be started.')
124 return False
125 # First read the data length as an unsigned 4-byte value. This
126 # is _not_ using network byte ordering since the Python test server packs
127 # size as native byte order and all Chromium platforms so far are
128 # configured to use little-endian.
129 # TODO(jnd): Change the Python test server and local_test_server_*.cc to
130 # use a unified byte order (either big-endian or little-endian).
131 data_length = os.read(self.pipe_in, struct.calcsize('=L'))
132 if data_length:
133 (data_length, ) = struct.unpack('=L', data_length)
bulach 2012/08/29 09:22:54 nit: no space after ,
Philippe 2012/08/29 11:29:05 Done.
134 assert data_length
135 if not data_length:
136 logging.error('Failed to get length of server data.')
137 return False
138 port_json = os.read(self.pipe_in, data_length)
139 if not port_json:
140 logging.error('Failed to get server data.')
141 return False
142 logging.info('Got port json data: %s' % port_json)
bulach 2012/08/29 09:22:54 nit: logging can do the formatting itself, so use
Philippe 2012/08/29 11:29:05 Done.
143 port_json = json.loads(port_json)
144 if port_json.has_key('port') and isinstance(port_json['port'], int):
145 self.host_port = port_json['port']
146 return CheckPortStatus(self.host_port, True)
147 logging.error('Failed to get port information from the server data.')
148 return False
149
150 def GenerateCommandLineArguments(self):
151 """Generate the command lines based on the input arguments to run
bulach 2012/08/29 09:22:54 nit: one line docstring
Philippe 2012/08/29 11:29:05 Done.
152 the test server. All options are processed by following the definitions
153 in testserver.py"""
154 if self.command_line:
155 return
156 # The following arguments must exist.
157 type_cmd = GetServerTypeCommandLine(self.arguments['server-type'])
158 if type_cmd:
159 self.command_line.append(type_cmd)
160 self.command_line.append('--port=%d' % self.host_port)
161 # Use a pipe to get the port given by the instance of Python test server
162 # if the test does not specify the port.
163 if self.host_port == 0:
164 (self.pipe_in, self.pipe_out) = os.pipe()
165 self.command_line.append('--startup-pipe=%d' % self.pipe_out)
166 self.command_line.append('--host=%s' % self.arguments['host'])
167 data_dir = self.arguments['data-dir'] or 'chrome/test/data'
168 if not os.path.isabs(data_dir):
169 data_dir = os.path.join(constants.CHROME_DIR, data_dir)
170 self.command_line.append('--data-dir=%s' % data_dir)
171 # The following arguments are optional depending on the individual test.
172 if self.arguments.has_key('log-to-console'):
173 self.command_line.append('--log-to-console')
174 if self.arguments.has_key('auth-token'):
175 self.command_line.append('--auth-token=%s' % self.arguments['auth-token'])
176 if self.arguments.has_key('https'):
177 self.command_line.append('--https')
178 if self.arguments.has_key('cert-and-key-file'):
179 self.command_line.append('--cert-and-key-file=%s' % os.path.join(
180 constants.CHROME_DIR, self.arguments['cert-and-key-file']))
181 if self.arguments.has_key('ocsp'):
182 self.command_line.append('--ocsp=%s' % self.arguments['ocsp'])
183 if self.arguments.has_key('https-record-resume'):
184 self.command_line.append('--https-record-resume')
185 if self.arguments.has_key('ssl-client-auth'):
186 self.command_line.append('--ssl-client-auth')
187 if self.arguments.has_key('tls-intolerant'):
188 self.command_line.append('--tls-intolerant')
189 if self.arguments.has_key('ssl-client-ca'):
190 for ca in self.arguments['ssl-client-ca']:
191 self.command_line.append('--ssl-client-ca=%s' %
192 os.path.join(constants.CHROME_DIR, ca))
193 if self.arguments.has_key('ssl-bulk-cipher'):
194 for bulk_cipher in self.arguments['ssl-bulk-cipher']:
195 self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher)
196
197 def run(self):
198 logging.info('Start running the thread!')
199 self.wait_event.clear()
200 self.GenerateCommandLineArguments()
201 command = '%s %s' % (
202 os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver',
203 'testserver.py'),
204 ' '.join(self.command_line))
205 logging.info(command)
206 self.process = subprocess.Popen(command, shell=True)
207 if self.process:
208 if self.pipe_out:
209 self.is_ready = self.WaitToStartAndGetPortFromTestServer()
210 else:
211 self.is_ready = CheckPortStatus(self.host_port, True)
212 if self.is_ready:
213 self._test_server_forwarder = Forwarder(
214 self.adb, [(0, self.host_port)], self.tool, '127.0.0.1',
215 self.build_type)
216 # Check whether the forwarder is ready on the device.
217 self.is_ready = False
218 device_port = self._test_server_forwarder.DevicePortForHostPort(
219 self.host_port)
220 if device_port:
221 for timeout in range(1, 5):
222 if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'):
223 self.is_ready = True
224 self.forwarder_device_port = device_port
225 break
226 time.sleep(timeout)
227 # Wake up the request handler thread.
228 self.ready_event.set()
229 # Keep thread running until Stop() gets called.
230 while not self.stop_flag:
231 time.sleep(1)
232 if self.process.poll() == None:
bulach 2012/08/29 09:22:54 nit: "is None"
Philippe 2012/08/29 11:29:05 Done.
233 self.process.kill()
234 if self._test_server_forwarder:
235 self._test_server_forwarder.Close()
236 self.process = None
237 self.is_ready = False
238 if self.pipe_out:
239 os.close(self.pipe_in)
240 os.close(self.pipe_out)
241 self.pipe_in = None
242 self.pipe_out = None
243 logging.info('Test-server has died.')
244 self.wait_event.set()
245
246 def Stop(self):
247 """Blocks until the loop has finished. This must be called in another
bulach 2012/08/29 09:22:54 nit: one line
Philippe 2012/08/29 11:29:05 Done.
248 thread.
249 """
250 if not self.process:
251 return
252 self.stop_flag = True
253 self.wait_event.wait()
254
29 255
30 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 256 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
31 """A handler used to process http GET request. 257 """A handler used to process http GET/POST request.
bulach 2012/08/29 09:22:54 nit: one line
Philippe 2012/08/29 11:29:05 Done.
32 """ 258 """
33 259
34 def GetServerType(self, server_type): 260 def SendResponse(self, response_code, response_reason, additional_headers,
35 """Returns the server type to use when starting the test server. 261 contents):
36 262 """Generate response according to specified parameters and send it back
37 This function translate the command-line argument into the appropriate 263 to client.
bulach 2012/08/29 09:22:54 nit: one line
Philippe 2012/08/29 11:29:05 Done.
38 numerical constant. 264
39 # TODO(yfriedman): Do that translation! 265 Args:
266 response_code: number of the response status.
267 response_reason: string of reason description of the response.
268 additional_headers: dict of additional headers. Each key is the name of
269 the header, each value is the content of the header.
270 contents: string of the contents we want to send to client.
40 """ 271 """
41 if server_type: 272 self.send_response(response_code, response_reason)
42 pass 273 self.send_header('Content-Type', 'text/html')
43 return 0 274 # Specify the content-length as without it the http(s) response will not
275 # be completed properly (and the browser keeps expecting data).
276 self.send_header('Content-Length', len(contents))
277 for header_name in additional_headers:
278 self.send_header(header_name, additional_headers[header_name])
279 self.end_headers()
280 self.wfile.write(contents)
281 self.wfile.flush()
282
283 def StartTestServer(self):
bulach 2012/08/29 09:22:54 nit: needs docstring
Philippe 2012/08/29 11:29:05 Done.
284 logging.info('Handling request to spawn a test server.')
285 content_type = self.headers.getheader('content-type')
286 if content_type != 'application/json':
287 raise Exception('Bad content-type for start request.')
288 content_length = self.headers.getheader('content-length')
289 if not content_length:
290 content_length = 0
291 try:
292 content_length = int(content_length)
293 except:
294 raise Exception('Bad content-length for start request.')
295 logging.info(content_length)
296 test_server_argument_json = self.rfile.read(content_length)
297 logging.info(test_server_argument_json)
298 assert not self.server.test_server_instance
299 ready_event = threading.Event()
300 self.server.test_server_instance = TestServerThread(
301 ready_event,
302 json.loads(test_server_argument_json),
303 self.server.adb,
304 self.server.tool,
305 self.server.build_type)
306 self.server.test_server_instance.setDaemon(True)
307 self.server.test_server_instance.start()
308 ready_event.wait()
309 if self.server.test_server_instance.is_ready:
310 self.SendResponse(200, 'OK', {}, json.dumps(
311 {'port': self.server.test_server_instance.forwarder_device_port,
312 'message': 'started'}))
313 logging.info('Test server is running on port: %d.' %
bulach 2012/08/29 09:22:54 nit: as above, s/%/,/
Philippe 2012/08/29 11:29:05 Done.
314 self.server.test_server_instance.host_port)
315 else:
316 self.server.test_server_instance.Stop()
317 self.server.test_server_instance = None
318 self.SendResponse(500, 'Test Server Error.', {}, '')
319 logging.info('Encounter problem during starting a test server.')
320
321 def KillTestServer(self):
bulach 2012/08/29 09:22:54 nit: need docstring
Philippe 2012/08/29 11:29:05 Done.
322 # There should only ever be one test server at a time. This may do the
323 # wrong thing if we try and start multiple test servers.
324 if not self.server.test_server_instance:
325 return
326 port = self.server.test_server_instance.host_port
327 logging.info('Handling request to kill a test server on port: %d.' % port)
328 self.server.test_server_instance.Stop()
329 # Make sure the status of test server is correct before sending response.
330 if CheckPortStatus(port, False):
331 self.SendResponse(200, 'OK', {}, 'killed')
332 logging.info('Test server on port %d is killed' % port)
bulach 2012/08/29 09:22:54 nit: s/%/,/
Philippe 2012/08/29 11:29:05 Done.
333 else:
334 self.SendResponse(500, 'Test Server Error.', {}, '')
335 logging.info('Encounter problem during killing a test server.')
336 self.server.test_server_instance = None
337
338 def do_POST(self):
339 parsed_path = urlparse.urlparse(self.path)
340 action = parsed_path.path
341 logging.info('Action for POST method is: %s.' % action)
bulach 2012/08/29 09:22:54 nit: s/%/,/
Philippe 2012/08/29 11:29:05 Done.
342 if action == '/start':
343 self.StartTestServer()
344 else:
345 self.SendResponse(400, 'Unknown request.', {}, '')
346 logging.info('Encounter unknown request: %s.' % action)
bulach 2012/08/29 09:22:54 nit: s/%/,/
Philippe 2012/08/29 11:29:05 Done.
44 347
45 def do_GET(self): 348 def do_GET(self):
46 parsed_path = urlparse.urlparse(self.path) 349 parsed_path = urlparse.urlparse(self.path)
47 action = parsed_path.path 350 action = parsed_path.path
48 params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) 351 params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1)
49 logging.info('Action is: %s' % action) 352 logging.info('Action for GET method is: %s.' % action)
bulach 2012/08/29 09:22:54 nit: s/%/,/
Philippe 2012/08/29 11:29:05 Done.
50 if action == '/killserver': 353 for param in params:
51 # There should only ever be one test server at a time. This may do the 354 logging.info('%s=%s' % (param, params[param][0]))
bulach 2012/08/29 09:22:54 nit: s/%/,/
Philippe 2012/08/29 11:29:05 Done.
52 # wrong thing if we try and start multiple test servers. 355 expect_exist = False
bulach 2012/08/29 09:22:54 nit: unused..
Philippe 2012/08/29 11:29:05 Done.
53 _test_servers.pop().Stop() 356 if action == '/kill':
54 elif action == '/start': 357 self.KillTestServer()
55 logging.info('Handling request to spawn a test webserver') 358 elif action == '/ping':
56 for param in params: 359 # The ping handler is used to check whether the spawner server is ready
57 logging.info('%s=%s' % (param, params[param][0])) 360 # to serve the requests. We don't need to test the status of the test
58 s_type = 0 361 # server when handling ping request.
59 doc_root = None 362 self.SendResponse(200, 'OK', {}, 'ready')
60 if 'server_type' in params: 363 logging.info('Handled ping request and sent response.')
61 s_type = self.GetServerType(params['server_type'][0]) 364 else:
62 if 'doc_root' in params: 365 self.SendResponse(400, 'Unknown request', {}, '')
63 doc_root = params['doc_root'][0] 366 logging.info('Encounter unknown request: %s.' % action)
bulach 2012/08/29 09:22:54 nit: s/%/,/
Philippe 2012/08/29 11:29:05 Done.
64 self.webserver_thread = threading.Thread(
65 target=self.SpawnTestWebServer, args=(s_type, doc_root))
66 self.webserver_thread.setDaemon(True)
67 self.webserver_thread.start()
68 self.send_response(200, 'OK')
69 self.send_header('Content-type', 'text/html')
70 self.end_headers()
71 self.wfile.write('<html><head><title>started</title></head></html>')
72 logging.info('Returned OK!!!')
73
74 def SpawnTestWebServer(self, s_type, doc_root):
75 class Options(object):
76 log_to_console = True
77 server_type = s_type
78 port = self.server.test_server_port
79 data_dir = doc_root or 'chrome/test/data'
80 file_root_url = '/files/'
81 cert = False
82 policy_keys = None
83 policy_user = None
84 startup_pipe = None
85 options = Options()
86 logging.info('Listening on %d, type %d, data_dir %s' % (options.port,
87 options.server_type, options.data_dir))
88 testserver.main(options, None, server_list=_test_servers)
89 logging.info('Test-server has died.')
90 367
91 368
92 class SpawningServer(object): 369 class SpawningServer(object):
93 """The class used to start/stop a http server. 370 """The class used to start/stop a http server.
bulach 2012/08/29 09:22:54 nit: one line
Philippe 2012/08/29 11:29:05 Done.
94 """ 371 """
95 372
96 def __init__(self, test_server_spawner_port, test_server_port): 373 def __init__(self, test_server_spawner_port, adb, tool, build_type):
97 logging.info('Creating new spawner %d', test_server_spawner_port) 374 logging.info('Creating new spawner on port: %d.', test_server_spawner_port)
98 self.server = testserver.StoppableHTTPServer(('', test_server_spawner_port), 375 self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port),
99 SpawningServerRequestHandler) 376 SpawningServerRequestHandler)
100 self.port = test_server_spawner_port 377 self.port = test_server_spawner_port
101 self.server.test_server_port = test_server_port 378 self.server.adb = adb
379 self.server.tool = tool
380 self.server.test_server_instance = None
381 self.server.build_type = build_type
102 382
103 def Listen(self): 383 def Listen(self):
104 logging.info('Starting test server spawner') 384 logging.info('Starting test server spawner')
105 self.server.serve_forever() 385 self.server.serve_forever()
106 386
107 def Start(self): 387 def Start(self):
108 listener_thread = threading.Thread(target=self.Listen) 388 listener_thread = threading.Thread(target=self.Listen)
109 listener_thread.setDaemon(True) 389 listener_thread.setDaemon(True)
110 listener_thread.start() 390 listener_thread.start()
111 time.sleep(1) 391 time.sleep(1)
112 392
113 def Stop(self): 393 def Stop(self):
114 self.server.Stop() 394 if self.server.test_server_instance:
395 self.server.test_server_instance.Stop()
396 self.server.shutdown()
OLDNEW
« no previous file with comments | « build/android/pylib/base_test_runner.py ('k') | build/android/pylib/constants.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698