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

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