tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

remotecppunittests.py (11484B)


      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 
      7 import os
      8 import posixpath
      9 import subprocess
     10 import sys
     11 import traceback
     12 from zipfile import ZipFile
     13 
     14 import mozcrash
     15 import mozfile
     16 import mozinfo
     17 import mozlog
     18 import runcppunittests as cppunittests
     19 from mozdevice import ADBDeviceFactory, ADBProcessError, ADBTimeoutError
     20 
     21 try:
     22    from mozbuild.base import MozbuildObject
     23 
     24    build_obj = MozbuildObject.from_environment()
     25 except ImportError:
     26    build_obj = None
     27 
     28 
     29 class 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(f"Pushing {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(f"Pushing {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):
    104        env = self.build_core_environment({})
    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,
    123        prog,
    124        env,
    125        symbols_path=None,
    126        utility_path=None,
    127        interactive=False,
    128        timeout_factor=1,
    129    ):
    130        """
    131        Run a single C++ unit test program remotely.
    132 
    133        Arguments:
    134        * prog: The path to the test program to run.
    135        * env: The environment to use for running the program.
    136        * symbols_path: A path to a directory containing Breakpad-formatted
    137                        symbol files for producing stack traces on crash.
    138        * timeout_factor: An optional test-specific timeout multiplier.
    139 
    140        Return True if the program exits with a zero status, False otherwise.
    141        """
    142        basename = os.path.basename(prog)
    143        remote_bin = posixpath.join(self.remote_bin_dir, basename)
    144        self.log.test_start(basename)
    145        test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor
    146 
    147        try:
    148            output = self.device.shell_output(
    149                remote_bin, env=env, cwd=self.remote_home_dir, timeout=test_timeout
    150            )
    151            returncode = 0
    152        except ADBTimeoutError:
    153            raise
    154        except ADBProcessError as e:
    155            output = e.adb_process.stdout
    156            returncode = e.adb_process.exitcode
    157 
    158        self.log.process_output(basename, "\n%s" % output, command=[remote_bin])
    159        with mozfile.TemporaryDirectory() as tempdir:
    160            self.device.pull(self.remote_home_dir, tempdir)
    161            if mozcrash.check_for_crashes(tempdir, symbols_path, test_name=basename):
    162                self.log.test_end(basename, status="CRASH", expected="PASS")
    163                return False
    164        result = returncode == 0
    165        if not result:
    166            self.log.test_end(
    167                basename,
    168                status="FAIL",
    169                expected="PASS",
    170                message=("test failed with return code %s" % returncode),
    171            )
    172        else:
    173            self.log.test_end(basename, status="PASS", expected="PASS")
    174        return result
    175 
    176 
    177 class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions):
    178    def __init__(self):
    179        cppunittests.CPPUnittestOptions.__init__(self)
    180        defaults = {}
    181 
    182        self.add_option(
    183            "--deviceSerial",
    184            action="store",
    185            type="string",
    186            dest="device_serial",
    187            help="adb serial number of remote device. This is required "
    188            "when more than one device is connected to the host. "
    189            "Use 'adb devices' to see connected devices.",
    190        )
    191        defaults["device_serial"] = None
    192 
    193        self.add_option(
    194            "--adbPath",
    195            action="store",
    196            type="string",
    197            dest="adb_path",
    198            help="Path to adb binary.",
    199        )
    200        defaults["adb_path"] = None
    201 
    202        self.add_option(
    203            "--noSetup",
    204            action="store_false",
    205            dest="setup",
    206            help="Do not copy any files to device (to be used only if "
    207            "device is already setup).",
    208        )
    209        defaults["setup"] = True
    210 
    211        self.add_option(
    212            "--localLib",
    213            action="store",
    214            type="string",
    215            dest="local_lib",
    216            help="Location of libraries to push -- preferably stripped.",
    217        )
    218        defaults["local_lib"] = None
    219 
    220        self.add_option(
    221            "--apk",
    222            action="store",
    223            type="string",
    224            dest="local_apk",
    225            help="Local path to Firefox for Android APK.",
    226        )
    227        defaults["local_apk"] = None
    228 
    229        self.add_option(
    230            "--localBinDir",
    231            action="store",
    232            type="string",
    233            dest="local_bin",
    234            help="Local path to bin directory.",
    235        )
    236        defaults["local_bin"] = build_obj.bindir if build_obj is not None else None
    237 
    238        self.add_option(
    239            "--remoteTestRoot",
    240            action="store",
    241            type="string",
    242            dest="remote_test_root",
    243            help="Remote directory to use as test root "
    244            "(eg. /data/local/tmp/test_root).",
    245        )
    246 
    247        # /data/local/tmp/test_root is used because it is usually not
    248        # possible to set +x permissions on binaries on /mnt/sdcard
    249        # and since scope storage on Android 10 causes permission
    250        # errors on the sdcard.
    251        defaults["remote_test_root"] = "/data/local/tmp/test_root"
    252 
    253        self.add_option(
    254            "--addEnv",
    255            action="append",
    256            type="string",
    257            dest="add_env",
    258            help="additional remote environment variable definitions "
    259            '(eg. --addEnv "somevar=something")',
    260        )
    261        defaults["add_env"] = None
    262 
    263        self.set_defaults(**defaults)
    264 
    265 
    266 def run_test_harness(options, args):
    267    options.xre_path = os.path.abspath(options.xre_path)
    268    cppunittests.update_mozinfo()
    269    progs = cppunittests.extract_unittests_from_args(
    270        args, mozinfo.info, options.manifest_path
    271    )
    272    tester = RemoteCPPUnitTests(options, [item[0] for item in progs])
    273    result = tester.run_tests(
    274        progs,
    275        options.xre_path,
    276        options.symbols_path,
    277    )
    278    return result
    279 
    280 
    281 def main():
    282    parser = RemoteCPPUnittestOptions()
    283    mozlog.commandline.add_logging_group(parser)
    284    options, args = parser.parse_args()
    285    if not args:
    286        print(
    287            """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0],
    288            file=sys.stderr,
    289        )
    290        sys.exit(1)
    291    if options.local_lib is not None and not os.path.isdir(options.local_lib):
    292        print(
    293            """Error: --localLib directory %s not found""" % options.local_lib,
    294            file=sys.stderr,
    295        )
    296        sys.exit(1)
    297    if options.local_apk is not None and not os.path.isfile(options.local_apk):
    298        print("""Error: --apk file %s not found""" % options.local_apk, file=sys.stderr)
    299        sys.exit(1)
    300    if not options.xre_path:
    301        print("""Error: --xre-path is required""", file=sys.stderr)
    302        sys.exit(1)
    303 
    304    log = mozlog.commandline.setup_logging(
    305        "remotecppunittests", options, {"tbpl": sys.stdout}
    306    )
    307    try:
    308        result = run_test_harness(options, args)
    309    except Exception as e:
    310        log.error(str(e))
    311        traceback.print_exc()
    312        result = False
    313    sys.exit(0 if result else 1)
    314 
    315 
    316 if __name__ == "__main__":
    317    main()