#!/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/.

"""Utility functions for mozrunner"""

import mozinfo
import os
import sys

__all__ = ['findInPath', 'get_metadata_from_egg']


# python package method metadata by introspection
try:
    import pkg_resources

    def get_metadata_from_egg(module):
        ret = {}
        try:
            dist = pkg_resources.get_distribution(module)
        except pkg_resources.DistributionNotFound:
            return {}
        if dist.has_metadata("PKG-INFO"):
            key = None
            value = ""
            for line in dist.get_metadata("PKG-INFO").splitlines():
                # see http://www.python.org/dev/peps/pep-0314/
                if key == 'Description':
                    # descriptions can be long
                    if not line or line[0].isspace():
                        value += '\n' + line
                        continue
                    else:
                        key = key.strip()
                        value = value.strip()
                        ret[key] = value

                key, value = line.split(':', 1)
                key = key.strip()
                value = value.strip()
                ret[key] = value
        if dist.has_metadata("requires.txt"):
            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
        return ret
except ImportError:
    # package resources not avaialable
    def get_metadata_from_egg(module):
        return {}


def findInPath(fileName, path=os.environ['PATH']):
    """python equivalent of which; should really be in the stdlib"""
    dirs = path.split(os.pathsep)
    for dir in dirs:
        if os.path.isfile(os.path.join(dir, fileName)):
            return os.path.join(dir, fileName)
        if mozinfo.isWin:
            if os.path.isfile(os.path.join(dir, fileName + ".exe")):
                return os.path.join(dir, fileName + ".exe")

if __name__ == '__main__':
    for i in sys.argv[1:]:
        print findInPath(i)


def _find_marionette_in_args(*args, **kwargs):
    try:
        m = [a for a in args + tuple(kwargs.values()) if hasattr(a, 'session')][0]
    except IndexError:
        print("Can only apply decorator to function using a marionette object")
        raise
    return m


def _raw_log():
    import logging
    return logging.getLogger(__name__)


def test_environment(xrePath, env=None, crashreporter=True, debugger=False,
                     dmdPath=None, lsanPath=None, log=None):
    """
    populate OS environment variables for mochitest and reftests.

    Originally comes from automationutils.py. Don't use that for new code.
    """

    env = os.environ.copy() if env is None else env
    log = log or _raw_log()

    assert os.path.isabs(xrePath)

    if mozinfo.isMac:
        ldLibraryPath = os.path.join(os.path.dirname(xrePath), "MacOS")
    else:
        ldLibraryPath = xrePath

    envVar = None
    dmdLibrary = None
    preloadEnvVar = None
    if mozinfo.isUnix:
        envVar = "LD_LIBRARY_PATH"
        env['MOZILLA_FIVE_HOME'] = xrePath
        dmdLibrary = "libdmd.so"
        preloadEnvVar = "LD_PRELOAD"
    elif mozinfo.isMac:
        envVar = "DYLD_LIBRARY_PATH"
        dmdLibrary = "libdmd.dylib"
        preloadEnvVar = "DYLD_INSERT_LIBRARIES"
    elif mozinfo.isWin:
        envVar = "PATH"
        dmdLibrary = "dmd.dll"
        preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
    if envVar:
        envValue = ((env.get(envVar), str(ldLibraryPath))
                    if mozinfo.isWin
                    else (ldLibraryPath, dmdPath, env.get(envVar)))
        env[envVar] = os.path.pathsep.join([path for path in envValue if path])

    if dmdPath and dmdLibrary and preloadEnvVar:
        env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)

    # crashreporter
    env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
    env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'

    if crashreporter and not debugger:
        env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
    else:
        env['MOZ_CRASHREPORTER_DISABLE'] = '1'

    # Crash on non-local network connections by default.
    # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
    # enable non-local connections for the purposes of local testing.  Don't
    # override the user's choice here.  See bug 1049688.
    env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')

    # Set WebRTC logging in case it is not set yet
    env.setdefault(
        'MOZ_LOG',
        'signaling:3,mtransport:4,DataChannel:4,jsep:4,MediaPipelineFactory:4'
    )
    env.setdefault('R_LOG_LEVEL', '6')
    env.setdefault('R_LOG_DESTINATION', 'stderr')
    env.setdefault('R_LOG_VERBOSE', '1')

    # ASan specific environment stuff
    asan = bool(mozinfo.info.get("asan"))
    if asan and (mozinfo.isLinux or mozinfo.isMac):
        try:
            # Symbolizer support
            llvmsym = os.path.join(xrePath, "llvm-symbolizer")
            if os.path.isfile(llvmsym):
                env["ASAN_SYMBOLIZER_PATH"] = llvmsym
                log.info("INFO | runtests.py | ASan using symbolizer at %s"
                         % llvmsym)
            else:
                log.info("TEST-UNEXPECTED-FAIL | runtests.py | Failed to find"
                         " ASan symbolizer at %s" % llvmsym)

            # Returns total system memory in kilobytes.
            # Works only on unix-like platforms where `free` is in the path.
            totalMemory = int(os.popen("free").readlines()[1].split()[1])

            # Only 4 GB RAM or less available? Use custom ASan options to reduce
            # the amount of resources required to do the tests. Standard options
            # will otherwise lead to OOM conditions on the current test slaves.
            message = "INFO | runtests.py | ASan running in %s configuration"
            asanOptions = []
            if totalMemory <= 1024 * 1024 * 4:
                message = message % 'low-memory'
                asanOptions = [
                    'quarantine_size=50331648', 'malloc_context_size=5']
            else:
                message = message % 'default memory'

            if lsanPath:
                log.info("LSan enabled.")
                asanOptions.append('detect_leaks=1')
                lsanOptions = ["exitcode=0"]
                # Uncomment out the next line to report the addresses of leaked objects.
                # lsanOptions.append("report_objects=1")
                suppressionsFile = os.path.join(
                    lsanPath, 'lsan_suppressions.txt')
                if os.path.exists(suppressionsFile):
                    log.info("LSan using suppression file " + suppressionsFile)
                    lsanOptions.append("suppressions=" + suppressionsFile)
                else:
                    log.info("WARNING | runtests.py | LSan suppressions file"
                             " does not exist! " + suppressionsFile)
                env["LSAN_OPTIONS"] = ':'.join(lsanOptions)

            if len(asanOptions):
                env['ASAN_OPTIONS'] = ':'.join(asanOptions)

        except OSError as err:
            log.info("Failed determine available memory, disabling ASan"
                     " low-memory configuration: %s" % err.strerror)
        except:
            log.info("Failed determine available memory, disabling ASan"
                     " low-memory configuration")
        else:
            log.info(message)

    tsan = bool(mozinfo.info.get("tsan"))
    if tsan and mozinfo.isLinux:
        # Symbolizer support.
        llvmsym = os.path.join(xrePath, "llvm-symbolizer")
        if os.path.isfile(llvmsym):
            env["TSAN_OPTIONS"] = "external_symbolizer_path=%s" % llvmsym
            log.info("INFO | runtests.py | TSan using symbolizer at %s"
                     % llvmsym)
        else:
            log.info("TEST-UNEXPECTED-FAIL | runtests.py | Failed to find TSan"
                     " symbolizer at %s" % llvmsym)

    return env


def get_stack_fixer_function(utilityPath, symbolsPath):
    """
    Return a stack fixing function, if possible, to use on output lines.

    A stack fixing function checks if a line conforms to the output from
    MozFormatCodeAddressDetails.  If the line does not, the line is returned
    unchanged.  If the line does, an attempt is made to convert the
    file+offset into something human-readable (e.g. a function name).
    """
    if not mozinfo.info.get('debug'):
        return None

    def import_stack_fixer_module(module_name):
        sys.path.insert(0, utilityPath)
        module = __import__(module_name, globals(), locals(), [])
        sys.path.pop(0)
        return module

    if symbolsPath and os.path.exists(symbolsPath):
        # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad
        # symbol files).
        # This method is preferred for Tinderbox builds, since native
        # symbols may have been stripped.
        stack_fixer_module = import_stack_fixer_module(
            'fix_stack_using_bpsyms')

        def stack_fixer_function(line):
            return stack_fixer_module.fixSymbols(line, symbolsPath)

    elif mozinfo.isMac:
        # Run each line through fix_macosx_stack.py (uses atos).
        # This method is preferred for developer machines, so we don't
        # have to run "make buildsymbols".
        stack_fixer_module = import_stack_fixer_module(
            'fix_macosx_stack')

        def stack_fixer_function(line):
            return stack_fixer_module.fixSymbols(line)

    elif mozinfo.isLinux:
        # Run each line through fix_linux_stack.py (uses addr2line).
        # This method is preferred for developer machines, so we don't
        # have to run "make buildsymbols".
        stack_fixer_module = import_stack_fixer_module(
            'fix_linux_stack')

        def stack_fixer_function(line):
            return stack_fixer_module.fixSymbols(line)

    else:
        return None

    return stack_fixer_function
