#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.


# jit_test.py -- Python harness for JavaScript trace tests.

import os
import re
import sys
import traceback
from collections import namedtuple
from enum import Enum

if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
    from .tasks_unix import run_all_tests
else:
    from .tasks_win import run_all_tests

from .progressbar import NullProgressBar, ProgressBar
from .results import escape_cmdline
from .structuredlog import TestLogger
from .tempfile import TemporaryDirectory

TESTS_LIB_DIR = os.path.dirname(os.path.abspath(__file__))
JS_DIR = os.path.dirname(os.path.dirname(TESTS_LIB_DIR))
TOP_SRC_DIR = os.path.dirname(os.path.dirname(JS_DIR))
TEST_DIR = os.path.join(JS_DIR, "jit-test", "tests")
LIB_DIR = os.path.join(JS_DIR, "jit-test", "lib") + os.path.sep
MODULE_DIR = os.path.join(JS_DIR, "jit-test", "modules") + os.path.sep
SHELL_XDR = "shell.xdr"


class OutputStatus(Enum):
    OK = 1
    SKIPPED = 2
    FAILED = 3

    def __bool__(self):
        return self != OutputStatus.FAILED


# Backported from Python 3.1 posixpath.py


def _relpath(path, start=None):
    """Return a relative version of a path"""

    if not path:
        raise ValueError("no path specified")

    if start is None:
        start = os.curdir

    start_list = os.path.abspath(start).split(os.sep)
    path_list = os.path.abspath(path).split(os.sep)

    # Work out how much of the filepath is shared by start and path.
    i = len(os.path.commonprefix([start_list, path_list]))

    rel_list = [os.pardir] * (len(start_list) - i) + path_list[i:]
    if not rel_list:
        return os.curdir
    return os.path.join(*rel_list)


# Mapping of Python chars to their javascript string representation.
QUOTE_MAP = {
    "\\": "\\\\",
    "\b": "\\b",
    "\f": "\\f",
    "\n": "\\n",
    "\r": "\\r",
    "\t": "\\t",
    "\v": "\\v",
}

# Quote the string S, javascript style.


def js_quote(quote, s):
    result = quote
    for c in s:
        if c == quote:
            result += "\\" + quote
        elif c in QUOTE_MAP:
            result += QUOTE_MAP[c]
        else:
            result += c
    result += quote
    return result


os.path.relpath = _relpath


def extend_condition(condition, value):
    if condition:
        condition += " || "
    condition += f"({value})"
    return condition


class JitTest:
    VALGRIND_CMD = []
    paths = (d for d in os.environ["PATH"].split(os.pathsep))
    valgrinds = (os.path.join(d, "valgrind") for d in paths)
    if any(os.path.exists(p) for p in valgrinds):
        VALGRIND_CMD = [
            "valgrind",
            "-q",
            "--smc-check=all-non-file",
            "--error-exitcode=1",
            "--gen-suppressions=all",
            "--show-possibly-lost=no",
            "--leak-check=full",
        ]
        if os.uname()[0] == "Darwin":
            VALGRIND_CMD.append("--dsymutil=yes")

    del paths
    del valgrinds

    def __init__(self, path):
        # Absolute path of the test file.
        self.path = path

        # Path relative to the top mozilla/ directory.
        self.relpath_top = os.path.relpath(path, TOP_SRC_DIR)

        # Path relative to mozilla/js/src/jit-test/tests/.
        self.relpath_tests = os.path.relpath(path, TEST_DIR)

        # jit flags to enable
        self.jitflags = []
        # True means the test is slow-running
        self.slow = False
        # Heavy tests will never run alongside other heavy tests
        self.heavy = False
        # True means that OOM is not considered a failure
        self.allow_oom = False
        # True means CrashAtUnhandlableOOM is not considered a failure
        self.allow_unhandlable_oom = False
        # True means that hitting recursion the limits is not considered a failure.
        self.allow_overrecursed = False
        # True means run under valgrind
        self.valgrind = False
        # True means force Pacific time for the test
        self.tz_pacific = False
        # Additional files to include, in addition to prologue.js
        self.other_lib_includes = []
        self.other_script_includes = []
        # List of other configurations to test with.
        self.test_also = []
        # List of other configurations to test with all existing variants.
        self.test_join = []
        # Errors to expect and consider passing
        self.expect_error = ""
        # Exit status to expect from shell
        self.expect_status = 0
        # Exit status or error output.
        self.expect_crash = False
        self.is_module = False
        # Reflect.stringify implementation to test
        self.test_reflect_stringify = None
        # Use self-hosted XDR instead of parsing the source stored in the binary.
        self.selfhosted_xdr_path = None
        self.selfhosted_xdr_mode = "off"

        # Skip-if condition. We don't have a xulrunner, but we can ask the shell
        # directly.
        self.skip_if_cond = ""
        self.skip_variant_if_cond = {}

        # Expected by the test runner. Always true for jit-tests.
        self.enable = True

    def copy(self):
        t = JitTest(self.path)
        t.jitflags = self.jitflags[:]
        t.slow = self.slow
        t.heavy = self.heavy
        t.allow_oom = self.allow_oom
        t.allow_unhandlable_oom = self.allow_unhandlable_oom
        t.allow_overrecursed = self.allow_overrecursed
        t.valgrind = self.valgrind
        t.tz_pacific = self.tz_pacific
        t.other_lib_includes = self.other_lib_includes[:]
        t.other_script_includes = self.other_script_includes[:]
        t.test_also = self.test_also
        t.test_join = self.test_join
        t.expect_error = self.expect_error
        t.expect_status = self.expect_status
        t.expect_crash = self.expect_crash
        t.test_reflect_stringify = self.test_reflect_stringify
        t.selfhosted_xdr_path = self.selfhosted_xdr_path
        t.selfhosted_xdr_mode = self.selfhosted_xdr_mode
        t.enable = True
        t.is_module = self.is_module
        t.skip_if_cond = self.skip_if_cond
        t.skip_variant_if_cond = self.skip_variant_if_cond
        return t

    def copy_and_extend_jitflags(self, variant):
        t = self.copy()
        t.jitflags.extend(variant)
        for flags in variant:
            if flags in self.skip_variant_if_cond:
                t.skip_if_cond = extend_condition(
                    t.skip_if_cond, self.skip_variant_if_cond[flags]
                )
        return t

    def copy_variants(self, variants):
        # Append variants to be tested in addition to the current set of tests.
        variants = variants + self.test_also

        # For each existing variant, duplicates it for each list of options in
        # test_join.  This will multiply the number of variants by 2 for set of
        # options.
        for join_opts in self.test_join:
            variants = variants + [opts + join_opts for opts in variants]

        # For each list of jit flags, make a copy of the test.
        return [self.copy_and_extend_jitflags(v) for v in variants]

    COOKIE = b"|jit-test|"

    # We would use 500019 (5k19), but quit() only accepts values up to 127, due to fuzzers
    SKIPPED_EXIT_STATUS = 59
    Directives = {}

    @classmethod
    def find_directives(cls, file_name):
        meta = ""
        line = open(file_name, "rb").readline()
        i = line.find(cls.COOKIE)
        if i != -1:
            meta = ";" + line[i + len(cls.COOKIE) :].decode(errors="strict").strip("\n")
        return meta

    @classmethod
    def from_file(cls, path, options):
        test = cls(path)

        # If directives.txt exists in the test's directory then it may
        # contain metainformation that will be catenated with
        # whatever's in the test file.  The form of the directive in
        # the directive file is the same as in the test file.  Only
        # the first line is considered, just as for the test file.

        dir_meta = ""
        dir_name = os.path.dirname(path)
        if dir_name in cls.Directives:
            dir_meta = cls.Directives[dir_name]
        else:
            meta_file_name = os.path.join(dir_name, "directives.txt")
            if os.path.exists(meta_file_name):
                dir_meta = cls.find_directives(meta_file_name)
            cls.Directives[dir_name] = dir_meta

        filename, file_extension = os.path.splitext(path)
        meta = cls.find_directives(path)

        if meta != "" or dir_meta != "":
            meta = meta + dir_meta
            parts = meta.split(";")
            for part in parts:
                part = part.strip()
                if not part:
                    continue
                name, _, value = part.partition(":")
                if value:
                    value = value.strip()
                    if name == "error":
                        test.expect_error = value
                    elif name == "exitstatus":
                        try:
                            status = int(value, 0)
                            if status == test.SKIPPED_EXIT_STATUS:
                                print(
                                    "warning: jit-tests uses {} as a sentinel"
                                    " return value {}",
                                    test.SKIPPED_EXIT_STATUS,
                                    path,
                                )
                            else:
                                test.expect_status = status
                        except ValueError:
                            print(f"warning: couldn't parse exit status {value}")
                    elif name == "thread-count":
                        try:
                            test.jitflags.append(f"--thread-count={int(value, 0)}")
                        except ValueError:
                            print(f"warning: couldn't parse thread-count {value}")
                    elif name == "include":
                        test.other_lib_includes.append(value)
                    elif name == "local-include":
                        test.other_script_includes.append(value)
                    elif name == "skip-if":
                        test.skip_if_cond = extend_condition(test.skip_if_cond, value)
                    elif name == "skip-variant-if":
                        try:
                            [variant, condition] = value.split(",")
                            test.skip_variant_if_cond[variant] = extend_condition(
                                test.skip_if_cond, condition
                            )
                        except ValueError:
                            print("warning: couldn't parse skip-variant-if")
                    else:
                        print(
                            f"{path}: warning: unrecognized |jit-test| attribute {part}"
                        )
                elif name == "slow":
                    test.slow = True
                elif name == "heavy":
                    test.heavy = True
                elif name == "allow-oom":
                    test.allow_oom = True
                elif name == "allow-unhandlable-oom":
                    test.allow_unhandlable_oom = True
                elif name == "allow-overrecursed":
                    test.allow_overrecursed = True
                elif name == "valgrind":
                    test.valgrind = options.valgrind
                elif name == "tz-pacific":
                    test.tz_pacific = True
                elif name.startswith("test-also="):
                    test.test_also.append(re.split(r"\s+", name[len("test-also=") :]))
                elif name.startswith("test-join="):
                    test.test_join.append(re.split(r"\s+", name[len("test-join=") :]))
                elif name == "module":
                    test.is_module = True
                elif name == "crash":
                    # Crashes are only allowed in self-test, as it is
                    # intended to verify that our testing infrastructure
                    # works, and not meant as a way to accept temporary
                    # failing tests. These tests should either be fixed or
                    # skipped.
                    assert "self-test" in path, (
                        f"{path}: has an unexpected crash annotation."
                    )
                    test.expect_crash = True
                elif name.startswith("--"):
                    # // |jit-test| --ion-gvn=off; --no-sse4
                    test.jitflags.append(name)
                elif name.startswith("-P"):
                    prefAndValue = name.split()
                    assert len(prefAndValue) == 2, f"{name}: failed to parse preference"
                    # // |jit-test| -P pref(=value)?
                    test.jitflags.append("--setpref=" + prefAndValue[1])
                else:
                    print(f"{path}: warning: unrecognized |jit-test| attribute {part}")

        if options.valgrind_all:
            test.valgrind = True

        if options.test_reflect_stringify is not None:
            test.expect_error = ""
            test.expect_status = 0

        return test

    def command(self, prefix, libdir, moduledir, tempdir, remote_prefix=None):
        path = self.path
        if remote_prefix:
            path = self.path.replace(TEST_DIR, remote_prefix)

        scriptdir_var = os.path.dirname(path)
        if not scriptdir_var.endswith("/"):
            scriptdir_var += "/"

        # Note: The tempdir provided as argument is managed by the caller
        # should remain alive as long as the test harness. Therefore, the XDR
        # content of the self-hosted code would be accessible to all JS Shell
        # instances.
        self.selfhosted_xdr_path = os.path.join(tempdir, SHELL_XDR)

        # Platforms where subprocess immediately invokes exec do not care
        # whether we use double or single quotes. On windows and when using
        # a remote device, however, we have to be careful to use the quote
        # style that is the opposite of what the exec wrapper uses.
        if remote_prefix:
            quotechar = '"'
        else:
            quotechar = "'"

        # Don't merge the expressions: We want separate -e arguments to avoid
        # semicolons in the command line, bug 1351607.
        exprs = [
            f"const platform={js_quote(quotechar, sys.platform)}",
            f"const libdir={js_quote(quotechar, libdir)}",
            f"const scriptdir={js_quote(quotechar, scriptdir_var)}",
        ]

        # We may have specified '-a' or '-d' twice: once via --jitflags, once
        # via the "|jit-test|" line.  Remove dups because they are toggles.
        # Note: |dict.fromkeys(flags)| is similar to |set(flags)| but it
        # preserves order.
        cmd = prefix + []
        cmd += list(dict.fromkeys(self.jitflags))
        # Handle selfhosted XDR file.
        if self.selfhosted_xdr_mode != "off":
            cmd += [
                "--selfhosted-xdr-path",
                self.selfhosted_xdr_path,
                "--selfhosted-xdr-mode",
                self.selfhosted_xdr_mode,
            ]
        for expr in exprs:
            cmd += ["-e", expr]
        for inc in self.other_lib_includes:
            cmd += ["-f", libdir + inc]
        for inc in self.other_script_includes:
            cmd += ["-f", scriptdir_var + inc]
        if self.skip_if_cond:
            cmd += [
                "-e",
                f"if ({self.skip_if_cond}) quit({self.SKIPPED_EXIT_STATUS})",
            ]
        cmd += ["--module-load-path", moduledir]
        if self.is_module:
            cmd += ["--module", path]
        elif self.test_reflect_stringify is None:
            cmd += ["-f", path]
        else:
            cmd += ["--", self.test_reflect_stringify, "--check", path]

        if self.valgrind:
            cmd = self.VALGRIND_CMD + cmd

        if self.allow_unhandlable_oom or self.expect_crash:
            cmd += ["--suppress-minidump"]

        return cmd

    def get_command(self, prefix, tempdir):
        """Shim for the test runner."""
        return self.command(prefix, LIB_DIR, MODULE_DIR, tempdir)


def find_tests(substring=None):
    ans = []
    for dirpath, dirnames, filenames in os.walk(TEST_DIR):
        dirnames.sort()
        filenames.sort()
        if dirpath == ".":
            continue

        for filename in filenames:
            if not filename.endswith(".js"):
                continue
            if filename in ("shell.js", "browser.js"):
                continue
            test = os.path.join(dirpath, filename)
            if substring is None or substring in os.path.relpath(test, TEST_DIR):
                ans.append(test)
    return ans


def check_output(out, err, rc, timed_out, test, options):
    # Allow skipping to compose with other expected results
    if test.skip_if_cond:
        if rc == test.SKIPPED_EXIT_STATUS:
            return OutputStatus.SKIPPED

    if timed_out:
        relpath = os.path.normpath(test.relpath_tests).replace(os.sep, "/")
        if relpath in options.ignore_timeouts:
            return OutputStatus.OK
        return OutputStatus.FAILED

    if test.expect_error:
        # The shell exits with code 3 on uncaught exceptions.
        if rc != 3:
            return OutputStatus.FAILED

        return test.expect_error in err

    for line in out.split("\n"):
        if line.startswith("Trace stats check failed"):
            return OutputStatus.FAILED

    for line in err.split("\n"):
        if "Assertion failed:" in line:
            return OutputStatus.FAILED

    if test.expect_crash:
        # Python 3 on Windows interprets process exit codes as unsigned
        # integers, where Python 2 used to allow signed integers. Account for
        # each possibility here.
        if sys.platform == "win32" and rc in (3 - 2**31, 3 + 2**31):
            return OutputStatus.OK

        if sys.platform != "win32" and rc == -11:
            return OutputStatus.OK

        # When building with ASan enabled, ASan will convert the -11 returned
        # value to 1. As a work-around we look for the error output which
        # includes the crash reason.
        if rc == 1 and ("Hit MOZ_CRASH" in err or "Assertion failure:" in err):
            return OutputStatus.OK

        # When running jittests on Android, SEGV results in a return code of
        # 128 + 11 = 139. Due to a bug in tinybox, we have to check for 138 as
        # well.
        if rc in {139, 138}:
            return OutputStatus.OK

        # Crashing test should always crash as expected, otherwise this is an
        # error. The JS shell crash() function can be used to force the test
        # case to crash in unexpected configurations.
        return OutputStatus.FAILED

    if rc != test.expect_status:
        # Allow a non-zero exit code if we want to allow OOM, but only if we
        # actually got OOM.
        if (
            test.allow_oom
            and "out of memory" in err
            and "Assertion failure" not in err
            and "MOZ_CRASH" not in err
        ):
            return OutputStatus.OK

        # Allow a non-zero exit code if we want to allow unhandlable OOM, but
        # only if we actually got unhandlable OOM.
        if test.allow_unhandlable_oom and "MOZ_CRASH([unhandlable oom]" in err:
            return OutputStatus.OK

        # Allow a non-zero exit code if we want to all too-much-recursion and
        # the test actually over-recursed.
        if (
            test.allow_overrecursed
            and "too much recursion" in err
            and "Assertion failure" not in err
        ):
            return OutputStatus.OK

        # Allow a zero exit code if we are running under a sanitizer that
        # forces the exit status.
        if test.expect_status != 0 and options.unusable_error_status:
            return OutputStatus.OK

        return OutputStatus.FAILED

    return OutputStatus.OK


def print_automation_format(ok, res, slog):
    # Output test failures in a parsable format suitable for automation, eg:
    # TEST-RESULT | filename.js | Failure description (code N, args "--foobar")
    #
    # Example:
    # TEST-PASS | foo/bar/baz.js | (code 0, args "--ion-eager")
    # TEST-UNEXPECTED-FAIL | foo/bar/baz.js | TypeError: or something (code -9, args "--no-ion")
    # INFO exit-status     : 3
    # INFO timed-out       : False
    # INFO stdout          > foo
    # INFO stdout          > bar
    # INFO stdout          > baz
    # INFO stderr         2> TypeError: or something
    # TEST-UNEXPECTED-FAIL | jit_test.py: Test execution interrupted by user
    result = "TEST-PASS" if ok else "TEST-UNEXPECTED-FAIL"
    message = "Success" if ok else res.describe_failure()
    jitflags = " ".join(res.test.jitflags)
    print(
        f'{result} | {res.test.relpath_top} | {message} (code {res.rc}, args "{jitflags}") [{res.dt:.1f} s]'
    )

    details = {
        "message": message,
        "extra": {
            "jitflags": jitflags,
        },
    }
    if res.extra:
        details["extra"].update(res.extra)
    slog.test(res.test.relpath_tests, "PASS" if ok else "FAIL", res.dt, **details)

    # For failed tests, print as much information as we have, to aid debugging.
    if ok:
        return
    print(f"INFO exit-status     : {res.rc}")
    print(f"INFO timed-out       : {res.timed_out}")
    warnings = []
    for line in res.out.splitlines():
        # See Bug 1868693
        if line.startswith("WARNING") and "unused DT entry" in line:
            warnings.append(line)
            continue
        print("INFO stdout          > " + line.strip())
    for line in res.err.splitlines():
        # See Bug 1868693
        if line.startswith("WARNING") and "unused DT entry" in line:
            warnings.append(line)
            continue
        print("INFO stderr         2> " + line.strip())
    for line in warnings:
        print("INFO (warn-stderr)  2> " + line.strip())


def print_test_summary(num_tests, failures, complete, slow_tests, doing, options):
    def test_details(res):
        if options.show_failed:
            return escape_cmdline(res.cmd)
        return " ".join(res.test.jitflags + [res.test.relpath_tests])

    if failures:
        if options.write_failures:
            try:
                out = open(options.write_failures, "w")
                # Don't write duplicate entries when we are doing multiple
                # failures per job.
                written = set()
                for res in failures:
                    if res.test.path not in written:
                        out.write(os.path.relpath(res.test.path, TEST_DIR) + "\n")
                        if options.write_failure_output:
                            out.write(res.out)
                            out.write(res.err)
                            out.write("Exit code: " + str(res.rc) + "\n")
                        written.add(res.test.path)
                out.close()
            except OSError:
                sys.stderr.write(
                    "Exception thrown trying to write failure"
                    f" file '{options.write_failures}'\n"
                )
                traceback.print_exc()
                sys.stderr.write("---\n")

        print("FAILURES:")
        for res in failures:
            if not res.timed_out:
                print("    " + test_details(res))

        print("TIMEOUTS:")
        for res in failures:
            if res.timed_out:
                print("    " + test_details(res))
    else:
        print(
            "PASSED ALL"
            + ("" if complete else f" (partial run -- interrupted by user {doing})")
        )

    if options.format == "automation":
        num_failures = len(failures) if failures else 0
        print("Result summary:")
        print(f"Passed: {num_tests - num_failures:d}")
        print(f"Failed: {num_failures:d}")

    if num_tests != 0 and options.show_slow:
        threshold = options.slow_test_threshold
        fraction_fast = 1 - len(slow_tests) / num_tests
        print(f"{fraction_fast * 100:5.2f}% of tests ran in under {threshold}s")

        print(f"Slowest tests that took longer than {threshold}s:")
        slow_tests.sort(key=lambda res: res.dt, reverse=True)
        any = False
        for i in range(min(len(slow_tests), 20)):
            res = slow_tests[i]
            print(f"  {res.dt:6.2f} {test_details(res)}")
            any = True
        if not any:
            print("None")

    return not failures


def create_progressbar(num_tests, options):
    if (
        not options.hide_progress
        and not options.show_cmd
        and ProgressBar.conservative_isatty()
    ):
        fmt = [
            {"value": "PASS", "color": "green"},
            {"value": "FAIL", "color": "red"},
            {"value": "TIMEOUT", "color": "blue"},
            {"value": "SKIP", "color": "brightgray"},
        ]
        return ProgressBar(num_tests, fmt)
    return NullProgressBar()


def process_test_results(results, num_tests, pb, options, slog):
    failures = []
    timeouts = 0
    skipped = 0
    complete = False
    output_dict = {}
    doing = "before starting"
    slow_tests = []

    if num_tests == 0:
        pb.finish(True)
        complete = True
        return print_test_summary(
            num_tests, failures, complete, slow_tests, doing, options
        )

    try:
        for i, res in enumerate(results):
            status = check_output(
                res.out, res.err, res.rc, res.timed_out, res.test, options
            )

            if status:
                show_output = options.show_output and not options.failed_only
            else:
                show_output = options.show_output or not options.no_show_failed

            if show_output:
                pb.beginline()
                sys.stdout.write(res.out)
                sys.stdout.write(res.err)
                sys.stdout.write(f"Exit code: {res.rc}\n")

            if res.test.valgrind and not show_output:
                pb.beginline()
                sys.stdout.write(res.err)

            if options.check_output:
                if res.test.path in output_dict.keys():
                    if output_dict[res.test.path] != res.out:
                        pb.message(f"FAIL - OUTPUT DIFFERS {res.test.relpath_tests}")
                else:
                    output_dict[res.test.path] = res.out

            doing = f"after {res.test.relpath_tests}"
            if status == OutputStatus.SKIPPED:
                skipped += 1
            elif status == OutputStatus.FAILED:
                failures.append(res)
                if res.timed_out:
                    pb.message(f"TIMEOUT - {res.test.relpath_tests}")
                    timeouts += 1
                else:
                    pb.message(f"FAIL - {res.test.relpath_tests}")

            if options.format == "automation":
                print_automation_format(status, res, slog)

            n = i + 1
            pb.update(
                n,
                {
                    "PASS": n - len(failures),
                    "FAIL": len(failures),
                    "TIMEOUT": timeouts,
                    "SKIP": skipped,
                },
            )

            if res.dt > options.slow_test_threshold:
                slow_tests.append(res)
        complete = True
    except KeyboardInterrupt:
        print(
            "TEST-UNEXPECTED-FAIL | jit_test.py"
            + " : Test execution interrupted by user"
        )

    pb.finish(True)
    return print_test_summary(num_tests, failures, complete, slow_tests, doing, options)


def run_tests(tests, num_tests, prefix, options, remote=False):
    slog = None
    if options.format == "automation":
        slog = TestLogger("jittests")
        slog.suite_start()

    if remote:
        ok = run_tests_remote(tests, num_tests, prefix, options, slog)
    else:
        ok = run_tests_local(tests, num_tests, prefix, options, slog)

    if slog:
        slog.suite_end()

    return ok


def run_tests_local(tests, num_tests, prefix, options, slog):
    # The jstests tasks runner requires the following options. The names are
    # taken from the jstests options processing code, which are frequently
    # subtly different from the options jit-tests expects. As such, we wrap
    # them here, as needed.
    AdaptorOptions = namedtuple(
        "AdaptorOptions",
        [
            "worker_count",
            "passthrough",
            "timeout",
            "output_fp",
            "hide_progress",
            "run_skipped",
            "show_cmd",
            "use_xdr",
        ],
    )
    shim_options = AdaptorOptions(
        options.max_jobs,
        False,
        options.timeout,
        sys.stdout,
        False,
        True,
        options.show_cmd,
        options.use_xdr,
    )

    with TemporaryDirectory() as tempdir:
        pb = create_progressbar(num_tests, options)
        gen = run_all_tests(tests, prefix, tempdir, pb, shim_options)
        ok = process_test_results(gen, num_tests, pb, options, slog)
    return ok


def run_tests_remote(tests, num_tests, prefix, options, slog):
    # Setup device with everything needed to run our tests.
    from mozdevice import ADBError, ADBTimeoutError

    from .tasks_adb_remote import get_remote_results

    # Run all tests.
    pb = create_progressbar(num_tests, options)
    try:
        gen = get_remote_results(tests, prefix, pb, options)
        ok = process_test_results(gen, num_tests, pb, options, slog)
    except (ADBError, ADBTimeoutError):
        print("TEST-UNEXPECTED-FAIL | jit_test.py" + " : Device error during test")
        raise
    return ok


if __name__ == "__main__":
    print("Use ../jit-test/jit_test.py to run these tests.")
