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