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