remotecppunittests.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!/usr/bin/env python
  2. #
  3. # This Source Code Form is subject to the terms of the Mozilla Public
  4. # License, v. 2.0. If a copy of the MPL was not distributed with this
  5. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. import os, sys
  7. import subprocess
  8. import tempfile
  9. from zipfile import ZipFile
  10. import runcppunittests as cppunittests
  11. import mozcrash
  12. import mozfile
  13. import mozinfo
  14. import mozlog
  15. import StringIO
  16. import posixpath
  17. from mozdevice import devicemanager, devicemanagerADB, devicemanagerSUT
  18. try:
  19. from mozbuild.base import MozbuildObject
  20. build_obj = MozbuildObject.from_environment()
  21. except ImportError:
  22. build_obj = None
  23. class RemoteCPPUnitTests(cppunittests.CPPUnitTests):
  24. def __init__(self, devmgr, options, progs):
  25. cppunittests.CPPUnitTests.__init__(self)
  26. self.options = options
  27. self.device = devmgr
  28. self.remote_test_root = self.device.deviceRoot + "/cppunittests"
  29. self.remote_bin_dir = posixpath.join(self.remote_test_root, "b")
  30. self.remote_tmp_dir = posixpath.join(self.remote_test_root, "tmp")
  31. self.remote_home_dir = posixpath.join(self.remote_test_root, "h")
  32. if options.setup:
  33. self.setup_bin(progs)
  34. def setup_bin(self, progs):
  35. if not self.device.dirExists(self.remote_test_root):
  36. self.device.mkDir(self.remote_test_root)
  37. if self.device.dirExists(self.remote_tmp_dir):
  38. self.device.removeDir(self.remote_tmp_dir)
  39. self.device.mkDir(self.remote_tmp_dir)
  40. if self.device.dirExists(self.remote_bin_dir):
  41. self.device.removeDir(self.remote_bin_dir)
  42. self.device.mkDir(self.remote_bin_dir)
  43. if self.device.dirExists(self.remote_home_dir):
  44. self.device.removeDir(self.remote_home_dir)
  45. self.device.mkDir(self.remote_home_dir)
  46. self.push_libs()
  47. self.push_progs(progs)
  48. self.device.chmodDir(self.remote_bin_dir)
  49. def push_libs(self):
  50. if self.options.local_apk:
  51. with mozfile.TemporaryDirectory() as tmpdir:
  52. apk_contents = ZipFile(self.options.local_apk)
  53. for info in apk_contents.infolist():
  54. if info.filename.endswith(".so"):
  55. print >> sys.stderr, "Pushing %s.." % info.filename
  56. remote_file = posixpath.join(self.remote_bin_dir, os.path.basename(info.filename))
  57. apk_contents.extract(info, tmpdir)
  58. local_file = os.path.join(tmpdir, info.filename)
  59. with open(local_file) as f:
  60. # Decompress xz-compressed file.
  61. if f.read(5)[1:] == '7zXZ':
  62. cmd = ['xz', '-df', '--suffix', '.so', local_file]
  63. subprocess.check_output(cmd)
  64. # xz strips the ".so" file suffix.
  65. os.rename(local_file[:-3], local_file)
  66. self.device.pushFile(local_file, remote_file)
  67. elif self.options.local_lib:
  68. for file in os.listdir(self.options.local_lib):
  69. if file.endswith(".so"):
  70. print >> sys.stderr, "Pushing %s.." % file
  71. remote_file = posixpath.join(self.remote_bin_dir, file)
  72. local_file = os.path.join(self.options.local_lib, file)
  73. self.device.pushFile(local_file, remote_file)
  74. # Additional libraries may be found in a sub-directory such as "lib/armeabi-v7a"
  75. for subdir in ["assets", "lib"]:
  76. local_arm_lib = os.path.join(self.options.local_lib, subdir)
  77. if os.path.isdir(local_arm_lib):
  78. for root, dirs, files in os.walk(local_arm_lib):
  79. for file in files:
  80. if (file.endswith(".so")):
  81. print >> sys.stderr, "Pushing %s.." % file
  82. remote_file = posixpath.join(self.remote_bin_dir, file)
  83. local_file = os.path.join(root, file)
  84. self.device.pushFile(local_file, remote_file)
  85. def push_progs(self, progs):
  86. for local_file in progs:
  87. remote_file = posixpath.join(self.remote_bin_dir, os.path.basename(local_file))
  88. self.device.pushFile(local_file, remote_file)
  89. def build_environment(self):
  90. env = self.build_core_environment()
  91. env['LD_LIBRARY_PATH'] = self.remote_bin_dir
  92. env["TMPDIR"]=self.remote_tmp_dir
  93. env["HOME"]=self.remote_home_dir
  94. env["MOZILLA_FIVE_HOME"] = self.remote_home_dir
  95. env["MOZ_XRE_DIR"] = self.remote_bin_dir
  96. if self.options.add_env:
  97. for envdef in self.options.add_env:
  98. envdef_parts = envdef.split("=", 1)
  99. if len(envdef_parts) == 2:
  100. env[envdef_parts[0]] = envdef_parts[1]
  101. elif len(envdef_parts) == 1:
  102. env[envdef_parts[0]] = ""
  103. else:
  104. self.log.warning("invalid --addEnv option skipped: %s" % envdef)
  105. return env
  106. def run_one_test(self, prog, env, symbols_path=None, interactive=False,
  107. timeout_factor=1):
  108. """
  109. Run a single C++ unit test program remotely.
  110. Arguments:
  111. * prog: The path to the test program to run.
  112. * env: The environment to use for running the program.
  113. * symbols_path: A path to a directory containing Breakpad-formatted
  114. symbol files for producing stack traces on crash.
  115. * timeout_factor: An optional test-specific timeout multiplier.
  116. Return True if the program exits with a zero status, False otherwise.
  117. """
  118. basename = os.path.basename(prog)
  119. remote_bin = posixpath.join(self.remote_bin_dir, basename)
  120. self.log.test_start(basename)
  121. buf = StringIO.StringIO()
  122. test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor
  123. returncode = self.device.shell([remote_bin], buf, env=env, cwd=self.remote_home_dir,
  124. timeout=test_timeout)
  125. self.log.process_output(basename, "\n%s" % buf.getvalue(),
  126. command=[remote_bin])
  127. with mozfile.TemporaryDirectory() as tempdir:
  128. self.device.getDirectory(self.remote_home_dir, tempdir)
  129. if mozcrash.check_for_crashes(tempdir, symbols_path,
  130. test_name=basename):
  131. self.log.test_end(basename, status='CRASH', expected='PASS')
  132. return False
  133. result = returncode == 0
  134. if not result:
  135. self.log.test_end(basename, status='FAIL', expected='PASS',
  136. message=("test failed with return code %s" %
  137. returncode))
  138. else:
  139. self.log.test_end(basename, status='PASS', expected='PASS')
  140. return result
  141. class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions):
  142. def __init__(self):
  143. cppunittests.CPPUnittestOptions.__init__(self)
  144. defaults = {}
  145. self.add_option("--deviceIP", action="store",
  146. type = "string", dest = "device_ip",
  147. help = "ip address of remote device to test")
  148. defaults["device_ip"] = None
  149. self.add_option("--devicePort", action="store",
  150. type = "string", dest = "device_port",
  151. help = "port of remote device to test")
  152. defaults["device_port"] = 20701
  153. self.add_option("--dm_trans", action="store",
  154. type = "string", dest = "dm_trans",
  155. help = "the transport to use to communicate with device: [adb|sut]; default=sut")
  156. defaults["dm_trans"] = "sut"
  157. self.add_option("--noSetup", action="store_false",
  158. dest = "setup",
  159. help = "do not copy any files to device (to be used only if device is already setup)")
  160. defaults["setup"] = True
  161. self.add_option("--localLib", action="store",
  162. type = "string", dest = "local_lib",
  163. help = "location of libraries to push -- preferably stripped")
  164. defaults["local_lib"] = None
  165. self.add_option("--apk", action="store",
  166. type = "string", dest = "local_apk",
  167. help = "local path to Fennec APK")
  168. defaults["local_apk"] = None
  169. self.add_option("--localBinDir", action="store",
  170. type = "string", dest = "local_bin",
  171. help = "local path to bin directory")
  172. defaults["local_bin"] = build_obj.bindir if build_obj is not None else None
  173. self.add_option("--remoteTestRoot", action = "store",
  174. type = "string", dest = "remote_test_root",
  175. help = "remote directory to use as test root (eg. /data/local/tests)")
  176. # /data/local/tests is used because it is usually not possible to set +x permissions
  177. # on binaries on /mnt/sdcard
  178. defaults["remote_test_root"] = "/data/local/tests"
  179. self.add_option("--with-b2g-emulator", action = "store",
  180. type = "string", dest = "with_b2g_emulator",
  181. help = "Start B2G Emulator (specify path to b2g home)")
  182. self.add_option("--emulator", default="arm", choices=["x86", "arm"],
  183. help = "Architecture of emulator to use: x86 or arm")
  184. self.add_option("--addEnv", action = "append",
  185. type = "string", dest = "add_env",
  186. help = "additional remote environment variable definitions (eg. --addEnv \"somevar=something\")")
  187. defaults["add_env"] = None
  188. self.set_defaults(**defaults)
  189. def run_test_harness(options, args):
  190. if options.with_b2g_emulator:
  191. from mozrunner import B2GEmulatorRunner
  192. runner = B2GEmulatorRunner(arch=options.emulator, b2g_home=options.with_b2g_emulator)
  193. runner.start()
  194. if options.dm_trans == "adb":
  195. if options.with_b2g_emulator:
  196. # because we just started the emulator, we need more than the
  197. # default number of retries here.
  198. retryLimit = 50
  199. else:
  200. retryLimit = 5
  201. try:
  202. if options.device_ip:
  203. dm = devicemanagerADB.DeviceManagerADB(options.device_ip, options.device_port, packageName=None, deviceRoot=options.remote_test_root, retryLimit=retryLimit)
  204. else:
  205. dm = devicemanagerADB.DeviceManagerADB(packageName=None, deviceRoot=options.remote_test_root, retryLimit=retryLimit)
  206. except:
  207. if options.with_b2g_emulator:
  208. runner.cleanup()
  209. runner.wait()
  210. raise
  211. else:
  212. dm = devicemanagerSUT.DeviceManagerSUT(options.device_ip, options.device_port, deviceRoot=options.remote_test_root)
  213. if not options.device_ip:
  214. print "Error: you must provide a device IP to connect to via the --deviceIP option"
  215. sys.exit(1)
  216. options.xre_path = os.path.abspath(options.xre_path)
  217. cppunittests.update_mozinfo()
  218. progs = cppunittests.extract_unittests_from_args(args,
  219. mozinfo.info,
  220. options.manifest_path)
  221. tester = RemoteCPPUnitTests(dm, options, [item[0] for item in progs])
  222. try:
  223. result = tester.run_tests(progs, options.xre_path, options.symbols_path)
  224. finally:
  225. if options.with_b2g_emulator:
  226. runner.cleanup()
  227. runner.wait()
  228. return result
  229. def main():
  230. parser = RemoteCPPUnittestOptions()
  231. mozlog.commandline.add_logging_group(parser)
  232. options, args = parser.parse_args()
  233. if not args:
  234. print >>sys.stderr, """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0]
  235. sys.exit(1)
  236. if options.local_lib is not None and not os.path.isdir(options.local_lib):
  237. print >>sys.stderr, """Error: --localLib directory %s not found""" % options.local_lib
  238. sys.exit(1)
  239. if options.local_apk is not None and not os.path.isfile(options.local_apk):
  240. print >>sys.stderr, """Error: --apk file %s not found""" % options.local_apk
  241. sys.exit(1)
  242. if not options.xre_path:
  243. print >>sys.stderr, """Error: --xre-path is required"""
  244. sys.exit(1)
  245. log = mozlog.commandline.setup_logging("remotecppunittests", options,
  246. {"tbpl": sys.stdout})
  247. try:
  248. result = run_test_harness(options, args)
  249. except Exception, e:
  250. log.error(str(e))
  251. result = False
  252. sys.exit(0 if result else 1)
  253. if __name__ == '__main__':
  254. main()