# Library for JSTest tests.
#
# This contains classes that represent an individual test, including
# metadata, and know how to run the tests and determine failures.

import os
import sys
from contextlib import contextmanager

# When run on tbpl, we run each test multiple times with the following
# arguments.
JITFLAGS = {
    "all": [
        [],  # no flags, normal baseline and ion
        [
            "--ion-eager",
            "--ion-offthread-compile=off",  # implies --baseline-eager
            "--more-compartments",
        ],
        [
            "--ion-eager",
            "--ion-offthread-compile=off",
            "--ion-check-range-analysis",
            "--ion-extra-checks",
            "--no-sse3",
            "--no-threads",
        ],
        ["--baseline-eager", "--write-protect-code=off"],
        ["--no-blinterp", "--no-baseline", "--no-ion", "--more-compartments"],
        ["--blinterp-eager"],
    ],
    # Like 'all' above but for jstests. This has fewer jit-specific
    # configurations.
    "jstests": [
        [],  # no flags, normal baseline and ion
        [
            "--ion-eager",
            "--ion-offthread-compile=off",  # implies --baseline-eager
            "--more-compartments",
        ],
        ["--baseline-eager", "--write-protect-code=off"],
        ["--no-blinterp", "--no-baseline", "--no-ion", "--more-compartments"],
    ],
    # used by jit_test.py
    "ion": [
        ["--baseline-eager", "--write-protect-code=off"],
        ["--ion-eager", "--ion-offthread-compile=off", "--more-compartments"],
    ],
    # Run reduced variants on debug builds, since they take longer time.
    "debug": [
        [],  # no flags, normal baseline and ion
        [
            "--ion-eager",
            "--ion-offthread-compile=off",  # implies --baseline-eager
            "--more-compartments",
        ],
        ["--baseline-eager", "--write-protect-code=off"],
    ],
    # Cover cases useful for tsan. Note that we test --ion-eager without
    # --ion-offthread-compile=off here, because it helps catch races.
    "tsan": [
        [],
        [
            "--ion-eager",
            "--ion-check-range-analysis",
            "--ion-extra-checks",
            "--no-sse3",
        ],
        ["--no-blinterp", "--no-baseline", "--no-ion"],
    ],
    "baseline": [
        ["--no-ion"],
    ],
    # Interpreter-only, for tools that cannot handle binary code generation.
    "interp": [
        [
            "--no-blinterp",
            "--no-baseline",
            "--no-asmjs",
            "--wasm-compiler=none",
            "--no-native-regexp",
        ]
    ],
    "none": [[]],  # no flags, normal baseline and ion
}


def get_jitflags(variant, **kwargs):
    if variant not in JITFLAGS:
        print(f'Invalid jitflag: "{variant}"')
        sys.exit(1)
    if variant == "none" and "none" in kwargs:
        return kwargs["none"]
    return JITFLAGS[variant]


def valid_jitflags():
    return JITFLAGS.keys()


def get_environment_overlay(js_shell, gc_zeal):
    """
    Build a dict of additional environment variables that must be set to run
    tests successfully.
    """

    # When updating this also update |buildBrowserEnv| in layout/tools/reftest/runreftest.py.
    env = {
        # Force Pacific time zone to avoid failures in Date tests.
        "TZ": "PST8PDT",
        # Force date strings to English.
        "LC_ALL": "en_US.UTF-8",
        # Tell the shell to disable crash dialogs on windows.
        "XRE_NO_WINDOWS_CRASH_DIALOG": "1",
    }

    # Add the binary's directory to the library search path so that we find the
    # nspr and icu we built, instead of the platform supplied ones (or none at
    # all on windows).
    if sys.platform.startswith("linux"):
        env["LD_LIBRARY_PATH"] = os.path.dirname(js_shell)
    elif sys.platform.startswith("darwin"):
        env["DYLD_LIBRARY_PATH"] = os.path.dirname(js_shell)
    elif sys.platform.startswith("win"):
        env["PATH"] = os.path.dirname(js_shell)

    if gc_zeal:
        env["JS_GC_ZEAL"] = gc_zeal

    return env


@contextmanager
def change_env(env_overlay):
    # Apply the overlaid environment and record the current state.
    prior_env = {}
    for key, val in env_overlay.items():
        prior_env[key] = os.environ.get(key, None)
        if "PATH" in key and key in os.environ:
            os.environ[key] = f"{val}{os.pathsep}{os.environ[key]}"
        else:
            os.environ[key] = val

    try:
        # Execute with the new environment.
        yield

    finally:
        # Restore the prior environment.
        for key, val in prior_env.items():
            if val is not None:
                os.environ[key] = val
            else:
                del os.environ[key]


def get_cpu_count():
    """
    Guess at a reasonable parallelism count to set as the default for the
    current machine and run.
    """
    # Python 2.6+
    try:
        import multiprocessing

        return multiprocessing.cpu_count()
    except (ImportError, NotImplementedError):
        pass

    # POSIX
    try:
        res = int(os.sysconf("SC_NPROCESSORS_ONLN"))
        if res > 0:
            return res
    except (AttributeError, ValueError):
        pass

    # Windows
    try:
        res = int(os.environ["NUMBER_OF_PROCESSORS"])
        if res > 0:
            return res
    except (KeyError, ValueError):
        pass

    return 1


class RefTestCase:
    """A test case consisting of a test and an expected result."""

    def __init__(self, root, path, extra_helper_paths=None, wpt=None):
        # str:  path of the tests root dir
        self.root = root
        # str:  path of JS file relative to tests root dir
        self.path = path
        # [str]: Extra options to pass to the shell
        self.options = []
        # [str]: JIT flags to pass to the shell
        self.jitflags = []
        # [str]: flags to never pass to the shell for this test
        self.ignoredflags = []
        # str or None: path to reflect-stringify.js file to test
        # instead of actually running tests
        self.test_reflect_stringify = None
        # bool: True => test is module code
        self.is_module = False
        # bool: True => test is asynchronous and runs additional code after completing the first
        # turn of the event loop.
        self.is_async = False
        # bool: True => run test, False => don't run
        self.enable = True
        # str?: Optional error type
        self.error = None
        # bool: expected result, True => pass
        self.expect = True
        # bool: True => ignore output as 'random'
        self.random = False
        # bool: True => test may run slowly
        self.slow = False
        # bool: True => test will not run alongside any other heavy tests
        self.heavy = False
        # bool: True => test is test262 testcase with raw flag, that turns off
        # running shell.js files inside test262
        self.is_test262_raw = False

        # Use self-hosted XDR instead of parsing the source stored in the binary.
        # str?: Path computed when generating the command
        self.selfhosted_xdr_path = None
        # str: XDR mode (= "off", "encode", "decode") to use with the
        # self-hosted code.
        self.selfhosted_xdr_mode = "off"

        # The terms parsed to produce the above properties.
        self.terms = None

        # The tag between |...| in the test header.
        self.tag = None

        # Anything occuring after -- in the test header.
        self.comment = None

        self.extra_helper_paths = extra_helper_paths or []
        self.wpt = wpt

    def prefix_command(self):
        """Return the '-f' options needed to run a test with the given path."""
        path = self.path
        prefix = []
        while path != "":
            assert path != "/"
            path = os.path.dirname(path)

            if self.is_test262_raw and path != "":
                # Skip running shell.js under test262 if the test has raw flag.
                # Top-level shell.js is still necessary to define reportCompare.
                continue

            shell_path = os.path.join(self.root, path, "shell.js")
            if os.path.exists(shell_path):
                prefix.append(shell_path)
                prefix.append("-f")
        prefix.reverse()

        for extra_path in self.extra_helper_paths:
            prefix.append("-f")
            prefix.append(extra_path)

        return prefix

    def abs_path(self):
        return os.path.join(self.root, self.path)

    def get_command(self, prefix, tempdir):
        cmd = prefix + self.jitflags + self.options + self.prefix_command()
        # 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.
        if self.selfhosted_xdr_mode != "off":
            self.selfhosted_xdr_path = os.path.join(tempdir, "shell.xdr")
            cmd += [
                "--selfhosted-xdr-path",
                self.selfhosted_xdr_path,
                "--selfhosted-xdr-mode",
                self.selfhosted_xdr_mode,
            ]
        if self.test_reflect_stringify is not None:
            cmd += [self.test_reflect_stringify, "--check", self.abs_path()]
        elif self.is_module:
            cmd += ["--module", self.abs_path()]
        else:
            cmd += ["-f", self.abs_path()]
        for flag in self.ignoredflags:
            if flag in cmd:
                cmd.remove(flag)
        return cmd

    def __str__(self):
        ans = self.path
        if not self.enable:
            ans += ", skip"
        if self.error is not None:
            ans += ", error=" + self.error
        if not self.expect:
            ans += ", fails"
        if self.random:
            ans += ", random"
        if self.slow:
            ans += ", slow"
        if self.heavy:
            ans += ", heavy"
        if "-d" in self.options:
            ans += ", debugMode"
        return ans

    @staticmethod
    def build_js_cmd_prefix(js_path, js_args, debugger_prefix):
        parts = []
        if debugger_prefix:
            parts += debugger_prefix
        parts.append(js_path)
        if js_args:
            parts += js_args
        return parts

    def __cmp__(self, other):
        if self.path == other.path:
            return 0
        elif self.path < other.path:
            return -1
        return 1

    def __hash__(self):
        return self.path.__hash__()

    def __repr__(self):
        return "<lib.tests.RefTestCase %s>" % (self.path,)
