Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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/.
7 import posixpath
8 import sys, os
9 import subprocess
10 import runxpcshelltests as xpcshell
11 import tempfile
12 from automationutils import replaceBackSlashes
13 from mozdevice import devicemanagerADB, devicemanagerSUT, devicemanager
14 from zipfile import ZipFile
15 import shutil
16 import mozfile
17 import mozinfo
19 here = os.path.dirname(os.path.abspath(__file__))
21 def remoteJoin(path1, path2):
22 return posixpath.join(path1, path2)
24 class RemoteXPCShellTestThread(xpcshell.XPCShellTestThread):
25 def __init__(self, *args, **kwargs):
26 xpcshell.XPCShellTestThread.__init__(self, *args, **kwargs)
28 # embed the mobile params from the harness into the TestThread
29 mobileArgs = kwargs.get('mobileArgs')
30 for key in mobileArgs:
31 setattr(self, key, mobileArgs[key])
33 def buildCmdTestFile(self, name):
34 remoteDir = self.remoteForLocal(os.path.dirname(name))
35 if remoteDir == self.remoteHere:
36 remoteName = os.path.basename(name)
37 else:
38 remoteName = remoteJoin(remoteDir, os.path.basename(name))
39 return ['-e', 'const _TEST_FILE = ["%s"];' %
40 replaceBackSlashes(remoteName)]
42 def remoteForLocal(self, local):
43 for mapping in self.pathMapping:
44 if (os.path.abspath(mapping.local) == os.path.abspath(local)):
45 return mapping.remote
46 return local
49 def setupTempDir(self):
50 # make sure the temp dir exists
51 if not self.device.dirExists(self.remoteTmpDir):
52 self.device.mkDir(self.remoteTmpDir)
53 # env var is set in buildEnvironment
54 return self.remoteTmpDir
56 def setupPluginsDir(self):
57 if not os.path.isdir(self.pluginsPath):
58 return None
60 # making sure tmp dir is set up
61 self.setupTempDir()
63 pluginsDir = remoteJoin(self.remoteTmpDir, "plugins")
64 self.device.pushDir(self.pluginsPath, pluginsDir)
65 if self.interactive:
66 self.log.info("TEST-INFO | plugins dir is %s" % pluginsDir)
67 return pluginsDir
69 def setupProfileDir(self):
70 self.device.removeDir(self.profileDir)
71 self.device.mkDir(self.profileDir)
72 if self.interactive or self.singleFile:
73 self.log.info("TEST-INFO | profile dir is %s" % self.profileDir)
74 return self.profileDir
76 def logCommand(self, name, completeCmd, testdir):
77 self.log.info("TEST-INFO | %s | full command: %r" % (name, completeCmd))
78 self.log.info("TEST-INFO | %s | current directory: %r" % (name, self.remoteHere))
79 self.log.info("TEST-INFO | %s | environment: %s" % (name, self.env))
81 def getHeadAndTailFiles(self, test):
82 """Override parent method to find files on remote device."""
83 def sanitize_list(s, kind):
84 for f in s.strip().split(' '):
85 f = f.strip()
86 if len(f) < 1:
87 continue
89 path = remoteJoin(self.remoteHere, f)
90 if not self.device.fileExists(path):
91 raise Exception('%s file does not exist: %s' % ( kind,
92 path))
94 yield path
96 self.remoteHere = self.remoteForLocal(test['here'])
98 return (list(sanitize_list(test['head'], 'head')),
99 list(sanitize_list(test['tail'], 'tail')))
101 def buildXpcsCmd(self, testdir):
102 # change base class' paths to remote paths and use base class to build command
103 self.xpcshell = remoteJoin(self.remoteBinDir, "xpcw")
104 self.headJSPath = remoteJoin(self.remoteScriptsDir, 'head.js')
105 self.httpdJSPath = remoteJoin(self.remoteComponentsDir, 'httpd.js')
106 self.httpdManifest = remoteJoin(self.remoteComponentsDir, 'httpd.manifest')
107 self.testingModulesDir = self.remoteModulesDir
108 self.testharnessdir = self.remoteScriptsDir
109 xpcshell.XPCShellTestThread.buildXpcsCmd(self, testdir)
110 # remove "-g <dir> -a <dir>" and add "--greomni <apk>"
111 del(self.xpcsCmd[1:5])
112 if self.options.localAPK:
113 self.xpcsCmd.insert(3, '--greomni')
114 self.xpcsCmd.insert(4, self.remoteAPK)
116 if self.remoteDebugger:
117 # for example, "/data/local/gdbserver" "localhost:12345"
118 self.xpcsCmd = [
119 self.remoteDebugger,
120 self.remoteDebuggerArgs,
121 self.xpcsCmd]
123 def testTimeout(self, test_file, proc):
124 self.timedout = True
125 if not self.retry:
126 self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file)
127 self.kill(proc)
129 def launchProcess(self, cmd, stdout, stderr, env, cwd):
130 self.timedout = False
131 cmd.insert(1, self.remoteHere)
132 outputFile = "xpcshelloutput"
133 with open(outputFile, 'w+') as f:
134 try:
135 self.shellReturnCode = self.device.shell(cmd, f)
136 except devicemanager.DMError as e:
137 if self.timedout:
138 # If the test timed out, there is a good chance the SUTagent also
139 # timed out and failed to return a return code, generating a
140 # DMError. Ignore the DMError to simplify the error report.
141 self.shellReturnCode = None
142 pass
143 else:
144 raise e
145 # The device manager may have timed out waiting for xpcshell.
146 # Guard against an accumulation of hung processes by killing
147 # them here. Note also that IPC tests may spawn new instances
148 # of xpcshell.
149 self.device.killProcess(cmd[0])
150 self.device.killProcess("xpcshell")
151 return outputFile
153 def checkForCrashes(self,
154 dump_directory,
155 symbols_path,
156 test_name=None):
157 if not self.device.dirExists(self.remoteMinidumpDir):
158 # The minidumps directory is automatically created when Fennec
159 # (first) starts, so its lack of presence is a hint that
160 # something went wrong.
161 print "Automation Error: No crash directory (%s) found on remote device" % self.remoteMinidumpDir
162 # Whilst no crash was found, the run should still display as a failure
163 return True
164 with mozfile.TemporaryDirectory() as dumpDir:
165 self.device.getDirectory(self.remoteMinidumpDir, dumpDir)
166 crashed = xpcshell.XPCShellTestThread.checkForCrashes(self, dumpDir, symbols_path, test_name)
167 self.device.removeDir(self.remoteMinidumpDir)
168 self.device.mkDir(self.remoteMinidumpDir)
169 return crashed
171 def communicate(self, proc):
172 f = open(proc, "r")
173 contents = f.read()
174 f.close()
175 os.remove(proc)
176 return contents, ""
178 def poll(self, proc):
179 if self.device.processExist("xpcshell") is None:
180 return self.getReturnCode(proc)
181 # Process is still running
182 return None
184 def kill(self, proc):
185 return self.device.killProcess("xpcshell", True)
187 def getReturnCode(self, proc):
188 if self.shellReturnCode is not None:
189 return self.shellReturnCode
190 else:
191 return -1
193 def removeDir(self, dirname):
194 self.device.removeDir(dirname)
196 #TODO: consider creating a separate log dir. We don't have the test file structure,
197 # so we use filename.log. Would rather see ./logs/filename.log
198 def createLogFile(self, test, stdout):
199 try:
200 f = None
201 filename = test.replace('\\', '/').split('/')[-1] + ".log"
202 f = open(filename, "w")
203 f.write(stdout)
205 finally:
206 if f is not None:
207 f.close()
210 # A specialization of XPCShellTests that runs tests on an Android device
211 # via devicemanager.
212 class XPCShellRemote(xpcshell.XPCShellTests, object):
214 def __init__(self, devmgr, options, args, log=None):
215 xpcshell.XPCShellTests.__init__(self, log)
217 # Add Android version (SDK level) to mozinfo so that manifest entries
218 # can be conditional on android_version.
219 androidVersion = devmgr.shellCheckOutput(['getprop', 'ro.build.version.sdk'])
220 mozinfo.info['android_version'] = androidVersion
222 self.localLib = options.localLib
223 self.localBin = options.localBin
224 self.options = options
225 self.device = devmgr
226 self.pathMapping = []
227 self.remoteTestRoot = self.device.getTestRoot("xpcshell")
228 # remoteBinDir contains xpcshell and its wrapper script, both of which must
229 # be executable. Since +x permissions cannot usually be set on /mnt/sdcard,
230 # and the test root may be on /mnt/sdcard, remoteBinDir is set to be on
231 # /data/local, always.
232 self.remoteBinDir = "/data/local/xpcb"
233 # Terse directory names are used here ("c" for the components directory)
234 # to minimize the length of the command line used to execute
235 # xpcshell on the remote device. adb has a limit to the number
236 # of characters used in a shell command, and the xpcshell command
237 # line can be quite complex.
238 self.remoteTmpDir = remoteJoin(self.remoteTestRoot, "tmp")
239 self.remoteScriptsDir = self.remoteTestRoot
240 self.remoteComponentsDir = remoteJoin(self.remoteTestRoot, "c")
241 self.remoteModulesDir = remoteJoin(self.remoteTestRoot, "m")
242 self.remoteMinidumpDir = remoteJoin(self.remoteTestRoot, "minidumps")
243 self.profileDir = remoteJoin(self.remoteTestRoot, "p")
244 self.remoteDebugger = options.debugger
245 self.remoteDebuggerArgs = options.debuggerArgs
246 self.testingModulesDir = options.testingModulesDir
248 self.env = {}
250 if self.options.objdir:
251 self.xpcDir = os.path.join(self.options.objdir, "_tests/xpcshell")
252 elif os.path.isdir(os.path.join(here, 'tests')):
253 self.xpcDir = os.path.join(here, 'tests')
254 else:
255 print >> sys.stderr, "Couldn't find local xpcshell test directory"
256 sys.exit(1)
258 if options.localAPK:
259 self.localAPKContents = ZipFile(options.localAPK)
260 if options.setup:
261 self.setupUtilities()
262 self.setupModules()
263 self.setupTestDir()
264 self.setupMinidumpDir()
265 self.remoteAPK = None
266 if options.localAPK:
267 self.remoteAPK = remoteJoin(self.remoteBinDir, os.path.basename(options.localAPK))
268 self.setAppRoot()
270 # data that needs to be passed to the RemoteXPCShellTestThread
271 self.mobileArgs = {
272 'device': self.device,
273 'remoteBinDir': self.remoteBinDir,
274 'remoteScriptsDir': self.remoteScriptsDir,
275 'remoteComponentsDir': self.remoteComponentsDir,
276 'remoteModulesDir': self.remoteModulesDir,
277 'options': self.options,
278 'remoteDebugger': self.remoteDebugger,
279 'pathMapping': self.pathMapping,
280 'profileDir': self.profileDir,
281 'remoteTmpDir': self.remoteTmpDir,
282 'remoteMinidumpDir': self.remoteMinidumpDir,
283 }
284 if self.remoteAPK:
285 self.mobileArgs['remoteAPK'] = self.remoteAPK
287 def setLD_LIBRARY_PATH(self):
288 self.env["LD_LIBRARY_PATH"] = self.remoteBinDir
290 def pushWrapper(self):
291 # Rather than executing xpcshell directly, this wrapper script is
292 # used. By setting environment variables and the cwd in the script,
293 # the length of the per-test command line is shortened. This is
294 # often important when using ADB, as there is a limit to the length
295 # of the ADB command line.
296 localWrapper = tempfile.mktemp()
297 f = open(localWrapper, "w")
298 f.write("#!/system/bin/sh\n")
299 for envkey, envval in self.env.iteritems():
300 f.write("export %s=%s\n" % (envkey, envval))
301 f.write("cd $1\n")
302 f.write("echo xpcw: cd $1\n")
303 f.write("shift\n")
304 f.write("echo xpcw: xpcshell \"$@\"\n")
305 f.write("%s/xpcshell \"$@\"\n" % self.remoteBinDir)
306 f.close()
307 remoteWrapper = remoteJoin(self.remoteBinDir, "xpcw")
308 self.device.pushFile(localWrapper, remoteWrapper)
309 os.remove(localWrapper)
310 self.device.chmodDir(self.remoteBinDir)
312 def buildEnvironment(self):
313 self.buildCoreEnvironment()
314 self.setLD_LIBRARY_PATH()
315 self.env["MOZ_LINKER_CACHE"] = self.remoteBinDir
316 if self.options.localAPK and self.appRoot:
317 self.env["GRE_HOME"] = self.appRoot
318 self.env["XPCSHELL_TEST_PROFILE_DIR"] = self.profileDir
319 self.env["TMPDIR"] = self.remoteTmpDir
320 self.env["HOME"] = self.profileDir
321 self.env["XPCSHELL_TEST_TEMP_DIR"] = self.remoteTmpDir
322 self.env["XPCSHELL_MINIDUMP_DIR"] = self.remoteMinidumpDir
323 if self.options.setup:
324 self.pushWrapper()
326 def setAppRoot(self):
327 # Determine the application root directory associated with the package
328 # name used by the Fennec APK.
329 self.appRoot = None
330 packageName = None
331 if self.options.localAPK:
332 try:
333 packageName = self.localAPKContents.read("package-name.txt")
334 if packageName:
335 self.appRoot = self.device.getAppRoot(packageName.strip())
336 except Exception as detail:
337 print "unable to determine app root: " + str(detail)
338 pass
339 return None
341 def setupUtilities(self):
342 if (not self.device.dirExists(self.remoteBinDir)):
343 # device.mkDir may fail here where shellCheckOutput may succeed -- see bug 817235
344 try:
345 self.device.shellCheckOutput(["mkdir", self.remoteBinDir]);
346 except devicemanager.DMError:
347 # Might get a permission error; try again as root, if available
348 self.device.shellCheckOutput(["mkdir", self.remoteBinDir], root=True);
349 self.device.shellCheckOutput(["chmod", "777", self.remoteBinDir], root=True);
351 remotePrefDir = remoteJoin(self.remoteBinDir, "defaults/pref")
352 if (self.device.dirExists(self.remoteTmpDir)):
353 self.device.removeDir(self.remoteTmpDir)
354 self.device.mkDir(self.remoteTmpDir)
355 if (not self.device.dirExists(remotePrefDir)):
356 self.device.mkDirs(remoteJoin(remotePrefDir, "extra"))
357 if (not self.device.dirExists(self.remoteScriptsDir)):
358 self.device.mkDir(self.remoteScriptsDir)
359 if (not self.device.dirExists(self.remoteComponentsDir)):
360 self.device.mkDir(self.remoteComponentsDir)
362 local = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'head.js')
363 remoteFile = remoteJoin(self.remoteScriptsDir, "head.js")
364 self.device.pushFile(local, remoteFile)
366 local = os.path.join(self.localBin, "xpcshell")
367 remoteFile = remoteJoin(self.remoteBinDir, "xpcshell")
368 self.device.pushFile(local, remoteFile)
370 local = os.path.join(self.localBin, "components/httpd.js")
371 remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.js")
372 self.device.pushFile(local, remoteFile)
374 local = os.path.join(self.localBin, "components/httpd.manifest")
375 remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.manifest")
376 self.device.pushFile(local, remoteFile)
378 local = os.path.join(self.localBin, "components/test_necko.xpt")
379 remoteFile = remoteJoin(self.remoteComponentsDir, "test_necko.xpt")
380 self.device.pushFile(local, remoteFile)
382 if self.options.localAPK:
383 remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(self.options.localAPK))
384 self.device.pushFile(self.options.localAPK, remoteFile)
386 self.pushLibs()
388 def pushLibs(self):
389 pushed_libs_count = 0
390 if self.options.localAPK:
391 try:
392 dir = tempfile.mkdtemp()
393 szip = os.path.join(self.localBin, '..', 'host', 'bin', 'szip')
394 if not os.path.exists(szip):
395 # Tinderbox builds must run szip from the test package
396 szip = os.path.join(self.localBin, 'host', 'szip')
397 if not os.path.exists(szip):
398 # If the test package doesn't contain szip, it means files
399 # are not szipped in the test package.
400 szip = None
401 for info in self.localAPKContents.infolist():
402 if info.filename.endswith(".so"):
403 print >> sys.stderr, "Pushing %s.." % info.filename
404 remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(info.filename))
405 self.localAPKContents.extract(info, dir)
406 file = os.path.join(dir, info.filename)
407 if szip:
408 out = subprocess.check_output([szip, '-d', file], stderr=subprocess.STDOUT)
409 self.device.pushFile(os.path.join(dir, info.filename), remoteFile)
410 pushed_libs_count += 1
411 finally:
412 shutil.rmtree(dir)
413 return pushed_libs_count
415 for file in os.listdir(self.localLib):
416 if (file.endswith(".so")):
417 print >> sys.stderr, "Pushing %s.." % file
418 if 'libxul' in file:
419 print >> sys.stderr, "This is a big file, it could take a while."
420 remoteFile = remoteJoin(self.remoteBinDir, file)
421 self.device.pushFile(os.path.join(self.localLib, file), remoteFile)
422 pushed_libs_count += 1
424 # Additional libraries may be found in a sub-directory such as "lib/armeabi-v7a"
425 localArmLib = os.path.join(self.localLib, "lib")
426 if os.path.exists(localArmLib):
427 for root, dirs, files in os.walk(localArmLib):
428 for file in files:
429 if (file.endswith(".so")):
430 print >> sys.stderr, "Pushing %s.." % file
431 remoteFile = remoteJoin(self.remoteBinDir, file)
432 self.device.pushFile(os.path.join(root, file), remoteFile)
433 pushed_libs_count += 1
435 return pushed_libs_count
437 def setupModules(self):
438 if self.testingModulesDir:
439 self.device.pushDir(self.testingModulesDir, self.remoteModulesDir)
441 def setupTestDir(self):
442 print 'pushing %s' % self.xpcDir
443 try:
444 self.device.pushDir(self.xpcDir, self.remoteScriptsDir, retryLimit=10)
445 except TypeError:
446 # Foopies have an older mozdevice ver without retryLimit
447 self.device.pushDir(self.xpcDir, self.remoteScriptsDir)
449 def setupMinidumpDir(self):
450 if self.device.dirExists(self.remoteMinidumpDir):
451 self.device.removeDir(self.remoteMinidumpDir)
452 self.device.mkDir(self.remoteMinidumpDir)
454 def buildTestList(self):
455 xpcshell.XPCShellTests.buildTestList(self)
456 uniqueTestPaths = set([])
457 for test in self.alltests:
458 uniqueTestPaths.add(test['here'])
459 for testdir in uniqueTestPaths:
460 abbrevTestDir = os.path.relpath(testdir, self.xpcDir)
461 remoteScriptDir = remoteJoin(self.remoteScriptsDir, abbrevTestDir)
462 self.pathMapping.append(PathMapping(testdir, remoteScriptDir))
464 class RemoteXPCShellOptions(xpcshell.XPCShellOptions):
466 def __init__(self):
467 xpcshell.XPCShellOptions.__init__(self)
468 defaults = {}
470 self.add_option("--deviceIP", action="store",
471 type = "string", dest = "deviceIP",
472 help = "ip address of remote device to test")
473 defaults["deviceIP"] = None
475 self.add_option("--devicePort", action="store",
476 type = "string", dest = "devicePort",
477 help = "port of remote device to test")
478 defaults["devicePort"] = 20701
480 self.add_option("--dm_trans", action="store",
481 type = "string", dest = "dm_trans",
482 help = "the transport to use to communicate with device: [adb|sut]; default=sut")
483 defaults["dm_trans"] = "sut"
485 self.add_option("--objdir", action="store",
486 type = "string", dest = "objdir",
487 help = "local objdir, containing xpcshell binaries")
488 defaults["objdir"] = None
490 self.add_option("--apk", action="store",
491 type = "string", dest = "localAPK",
492 help = "local path to Fennec APK")
493 defaults["localAPK"] = None
495 self.add_option("--noSetup", action="store_false",
496 dest = "setup",
497 help = "do not copy any files to device (to be used only if device is already setup)")
498 defaults["setup"] = True
500 self.add_option("--local-lib-dir", action="store",
501 type = "string", dest = "localLib",
502 help = "local path to library directory")
503 defaults["localLib"] = None
505 self.add_option("--local-bin-dir", action="store",
506 type = "string", dest = "localBin",
507 help = "local path to bin directory")
508 defaults["localBin"] = None
510 self.add_option("--remoteTestRoot", action = "store",
511 type = "string", dest = "remoteTestRoot",
512 help = "remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
513 defaults["remoteTestRoot"] = None
515 self.set_defaults(**defaults)
517 def verifyRemoteOptions(self, options):
518 if options.localLib is None:
519 if options.localAPK and options.objdir:
520 for path in ['dist/fennec', 'fennec/lib']:
521 options.localLib = os.path.join(options.objdir, path)
522 if os.path.isdir(options.localLib):
523 break
524 else:
525 self.error("Couldn't find local library dir, specify --local-lib-dir")
526 elif options.objdir:
527 options.localLib = os.path.join(options.objdir, 'dist/bin')
528 elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
529 # assume tests are being run from a tests.zip
530 options.localLib = os.path.abspath(os.path.join(here, '..', 'bin'))
531 else:
532 self.error("Couldn't find local library dir, specify --local-lib-dir")
534 if options.localBin is None:
535 if options.objdir:
536 for path in ['dist/bin', 'bin']:
537 options.localBin = os.path.join(options.objdir, path)
538 if os.path.isdir(options.localBin):
539 break
540 else:
541 self.error("Couldn't find local binary dir, specify --local-bin-dir")
542 elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
543 # assume tests are being run from a tests.zip
544 options.localBin = os.path.abspath(os.path.join(here, '..', 'bin'))
545 else:
546 self.error("Couldn't find local binary dir, specify --local-bin-dir")
547 return options
549 class PathMapping:
551 def __init__(self, localDir, remoteDir):
552 self.local = localDir
553 self.remote = remoteDir
555 def main():
557 if sys.version_info < (2,7):
558 print >>sys.stderr, "Error: You must use python version 2.7 or newer but less than 3.0"
559 sys.exit(1)
561 parser = RemoteXPCShellOptions()
562 options, args = parser.parse_args()
563 if not options.localAPK:
564 for file in os.listdir(os.path.join(options.objdir, "dist")):
565 if (file.endswith(".apk") and file.startswith("fennec")):
566 options.localAPK = os.path.join(options.objdir, "dist")
567 options.localAPK = os.path.join(options.localAPK, file)
568 print >>sys.stderr, "using APK: " + options.localAPK
569 break
570 else:
571 print >>sys.stderr, "Error: please specify an APK"
572 sys.exit(1)
574 options = parser.verifyRemoteOptions(options)
576 if len(args) < 1 and options.manifest is None:
577 print >>sys.stderr, """Usage: %s <test dirs>
578 or: %s --manifest=test.manifest """ % (sys.argv[0], sys.argv[0])
579 sys.exit(1)
581 if (options.dm_trans == "adb"):
582 if (options.deviceIP):
583 dm = devicemanagerADB.DeviceManagerADB(options.deviceIP, options.devicePort, packageName=None, deviceRoot=options.remoteTestRoot)
584 else:
585 dm = devicemanagerADB.DeviceManagerADB(packageName=None, deviceRoot=options.remoteTestRoot)
586 else:
587 dm = devicemanagerSUT.DeviceManagerSUT(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
588 if (options.deviceIP == None):
589 print "Error: you must provide a device IP to connect to via the --device option"
590 sys.exit(1)
592 if options.interactive and not options.testPath:
593 print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
594 sys.exit(1)
596 xpcsh = XPCShellRemote(dm, options, args)
598 # we don't run concurrent tests on mobile
599 options.sequential = True
601 if not xpcsh.runTests(xpcshell='xpcshell',
602 testClass=RemoteXPCShellTestThread,
603 testdirs=args[0:],
604 mobileArgs=xpcsh.mobileArgs,
605 **options.__dict__):
606 sys.exit(1)
609 if __name__ == '__main__':
610 main()