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
3# found in the LICENSE file.
4
5import contextlib
6import httplib
7import logging
8import os
9import tempfile
10import time
11
12import android_commands
13import constants
14from chrome_test_server_spawner import SpawningServer
15import constants
16from flag_changer import FlagChanger
17from forwarder import Forwarder
18import lighttpd_server
19import ports
20from valgrind_tools import CreateTool
21
22
23# A file on device to store ports of net test server. The format of the file is
24# test-spawner-server-port:test-server-port
25NET_TEST_SERVER_PORT_INFO_FILE = 'net-test-server-ports'
26
27
28class BaseTestRunner(object):
29  """Base class for running tests on a single device.
30
31  A subclass should implement RunTests() with no parameter, so that calling
32  the Run() method will set up tests, run them and tear them down.
33  """
34
35  def __init__(self, device, tool, shard_index, build_type):
36    """
37      Args:
38        device: Tests will run on the device of this ID.
39        shard_index: Index number of the shard on which the test suite will run.
40        build_type: 'Release' or 'Debug'.
41    """
42    self.device = device
43    self.adb = android_commands.AndroidCommands(device=device)
44    self.tool = CreateTool(tool, self.adb)
45    self._http_server = None
46    self._forwarder = None
47    self._forwarder_device_port = 8000
48    self.forwarder_base_url = ('http://localhost:%d' %
49        self._forwarder_device_port)
50    self.flags = FlagChanger(self.adb)
51    self.shard_index = shard_index
52    self.flags.AddFlags(['--disable-fre'])
53    self._spawning_server = None
54    self._spawner_forwarder = None
55    # We will allocate port for test server spawner when calling method
56    # LaunchChromeTestServerSpawner and allocate port for test server when
57    # starting it in TestServerThread.
58    self.test_server_spawner_port = 0
59    self.test_server_port = 0
60    self.build_type = build_type
61
62  def _PushTestServerPortInfoToDevice(self):
63    """Pushes the latest port information to device."""
64    self.adb.SetFileContents(self.adb.GetExternalStorage() + '/' +
65                             NET_TEST_SERVER_PORT_INFO_FILE,
66                             '%d:%d' % (self.test_server_spawner_port,
67                                        self.test_server_port))
68
69  def Run(self):
70    """Calls subclass functions to set up tests, run them and tear them down.
71
72    Returns:
73      Test results returned from RunTests().
74    """
75    if not self.HasTests():
76      return True
77    self.SetUp()
78    try:
79      return self.RunTests()
80    finally:
81      self.TearDown()
82
83  def SetUp(self):
84    """Called before tests run."""
85    pass
86
87  def HasTests(self):
88    """Whether the test suite has tests to run."""
89    return True
90
91  def RunTests(self):
92    """Runs the tests. Need to be overridden."""
93    raise NotImplementedError
94
95  def TearDown(self):
96    """Called when tests finish running."""
97    self.ShutdownHelperToolsForTestSuite()
98
99  def CopyTestData(self, test_data_paths, dest_dir):
100    """Copies |test_data_paths| list of files/directories to |dest_dir|.
101
102    Args:
103      test_data_paths: A list of files or directories relative to |dest_dir|
104          which should be copied to the device. The paths must exist in
105          |CHROME_DIR|.
106      dest_dir: Absolute path to copy to on the device.
107    """
108    for p in test_data_paths:
109      self.adb.PushIfNeeded(
110          os.path.join(constants.CHROME_DIR, p),
111          os.path.join(dest_dir, p))
112
113  def LaunchTestHttpServer(self, document_root, port=None,
114                           extra_config_contents=None):
115    """Launches an HTTP server to serve HTTP tests.
116
117    Args:
118      document_root: Document root of the HTTP server.
119      port: port on which we want to the http server bind.
120      extra_config_contents: Extra config contents for the HTTP server.
121    """
122    self._http_server = lighttpd_server.LighttpdServer(
123        document_root, port=port, extra_config_contents=extra_config_contents)
124    if self._http_server.StartupHttpServer():
125      logging.info('http server started: http://localhost:%s',
126                   self._http_server.port)
127    else:
128      logging.critical('Failed to start http server')
129    self.StartForwarderForHttpServer()
130    return (self._forwarder_device_port, self._http_server.port)
131
132  def StartForwarder(self, port_pairs):
133    """Starts TCP traffic forwarding for the given |port_pairs|.
134
135    Args:
136      host_port_pairs: A list of (device_port, local_port) tuples to forward.
137    """
138    if self._forwarder:
139      self._forwarder.Close()
140    self._forwarder = Forwarder(
141        self.adb, port_pairs, self.tool, '127.0.0.1', self.build_type)
142
143  def StartForwarderForHttpServer(self):
144    """Starts a forwarder for the HTTP server.
145
146    The forwarder forwards HTTP requests and responses between host and device.
147    """
148    self.StartForwarder([(self._forwarder_device_port, self._http_server.port)])
149
150  def RestartHttpServerForwarderIfNecessary(self):
151    """Restarts the forwarder if it's not open."""
152    # Checks to see if the http server port is being used.  If not forwards the
153    # request.
154    # TODO(dtrainor): This is not always reliable because sometimes the port
155    # will be left open even after the forwarder has been killed.
156    if not ports.IsDevicePortUsed(self.adb,
157        self._forwarder_device_port):
158      self.StartForwarderForHttpServer()
159
160  def ShutdownHelperToolsForTestSuite(self):
161    """Shuts down the server and the forwarder."""
162    # Forwarders should be killed before the actual servers they're forwarding
163    # to as they are clients potentially with open connections and to allow for
164    # proper hand-shake/shutdown.
165    if self._forwarder or self._spawner_forwarder:
166      # Kill all forwarders on the device and then kill the process on the host
167      # (if it exists)
168      self.adb.KillAll('device_forwarder')
169      if self._forwarder:
170        self._forwarder.Close()
171      if self._spawner_forwarder:
172        self._spawner_forwarder.Close()
173    if self._http_server:
174      self._http_server.ShutdownHttpServer()
175    if self._spawning_server:
176      self._spawning_server.Stop()
177    self.flags.Restore()
178
179  def LaunchChromeTestServerSpawner(self):
180    """Launches test server spawner."""
181    server_ready = False
182    error_msgs = []
183    # Try 3 times to launch test spawner server.
184    for i in xrange(0, 3):
185      # Do not allocate port for test server here. We will allocate
186      # different port for individual test in TestServerThread.
187      self.test_server_spawner_port = ports.AllocateTestServerPort()
188      self._spawning_server = SpawningServer(self.test_server_spawner_port,
189                                             self.adb,
190                                             self.tool,
191                                             self.build_type)
192      self._spawning_server.Start()
193      server_ready, error_msg = ports.IsHttpServerConnectable(
194          '127.0.0.1', self.test_server_spawner_port, path='/ping',
195          expected_read='ready')
196      if server_ready:
197        break
198      else:
199        error_msgs.append(error_msg)
200      self._spawning_server.Stop()
201      # Wait for 2 seconds then restart.
202      time.sleep(2)
203    if not server_ready:
204      logging.error(';'.join(error_msgs))
205      raise Exception('Can not start the test spawner server.')
206    self._PushTestServerPortInfoToDevice()
207    self._spawner_forwarder = Forwarder(
208        self.adb,
209        [(self.test_server_spawner_port, self.test_server_spawner_port)],
210        self.tool, '127.0.0.1', self.build_type)
211