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


import posixpath
import shutil
import sys
import tempfile
import time
import traceback
from optparse import OptionParser

import mozcrash
import mozdevice
import mozlog
from mozprofile import Profile


class GeckoviewOptions(OptionParser):
    def __init__(self):
        OptionParser.__init__(self)
        self.add_option("--utility-path",
                        action="store", type="string", dest="utility_path",
                        default=None,
                        help="absolute path to directory containing utility programs")
        self.add_option("--symbols-path",
                        action="store", type="string", dest="symbols_path",
                        default=None,
                        help="absolute path to directory containing breakpad symbols, \
                              or the URL of a zip file containing symbols")
        self.add_option("--appname",
                        action="store", type="string", dest="app",
                        default="org.mozilla.geckoview_example",
                        help="geckoview_example package name")
        self.add_option("--deviceIP",
                        action="store", type="string", dest="deviceIP",
                        default=None,
                        help="ip address of remote device to test")
        self.add_option("--deviceSerial",
                        action="store", type="string", dest="deviceSerial",
                        default=None,
                        help="serial ID of remote device to test")
        self.add_option("--adbpath",
                        action="store", type="string", dest="adbPath",
                        default="adb",
                        help="Path to adb binary.")
        self.add_option("--remoteTestRoot",
                        action="store", type="string", dest="remoteTestRoot",
                        default=None,
                        help="remote directory to use as test root \
                              (eg. /mnt/sdcard/tests or /data/local/tests)")


class GeckoviewTestRunner:
    """
       A quick-and-dirty test harness to verify the geckoview_example
       app starts without crashing.
    """

    def __init__(self, log, dm, options):
        self.log = log
        self.dm = dm
        self.options = options
        self.appname = self.options.app.split('/')[-1]
        self.logcat = None
        self.build_profile()
        self.log.debug("options=%s" % vars(options))

    def build_profile(self):
        test_root = self.dm.deviceRoot
        self.remote_profile = posixpath.join(test_root, 'gv-profile')
        self.dm.mkDirs(posixpath.join(self.remote_profile, "x"))
        profile = Profile()
        self.dm.pushDir(profile.profile, self.remote_profile)
        self.log.debug("profile %s -> %s" %
                       (str(profile.profile), str(self.remote_profile)))

    def installed(self):
        """
        geckoview_example installed
        """
        installed = self.dm.shellCheckOutput(['pm', 'list', 'packages', self.appname])
        if self.appname not in installed:
            return (False, "%s not installed" % self.appname)
        return (True, "%s installed" % self.appname)

    def start(self):
        """
        geckoview_example starts
        """
        try:
            self.dm.stopApplication(self.appname)
            self.dm.recordLogcat()
            cmd = ['am', 'start', '-a', 'android.intent.action.MAIN', '-n',
                   'org.mozilla.geckoview_example/org.mozilla.geckoview_example.GeckoViewActivity',
                   '--es', 'args', '-profile %s' % self.remote_profile]
            env = {}
            env["MOZ_CRASHREPORTER"] = 1
            env["MOZ_CRASHREPORTER_NO_REPORT"] = 1
            env["XPCOM_DEBUG_BREAK"] = "stack"
            env["DISABLE_UNSAFE_CPOW_WARNINGS"] = 1
            env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = 1
            env["MOZ_IN_AUTOMATION"] = 1
            env["R_LOG_VERBOSE"] = 1
            env["R_LOG_LEVEL"] = 6
            env["R_LOG_DESTINATION"] = "stderr"
            i = 0
            for key, value in env.iteritems():
                cmd.append("--es")
                cmd.append("env%d" % i)
                cmd.append("%s=%s" % (key, str(value)))
                i = i + 1
            self.dm.shellCheckOutput(cmd)
        except mozdevice.DMError:
            return (False, "Exception during %s startup" % self.appname)
        return (True, "%s started" % self.appname)

    def started(self):
        """
        startup logcat messages
        """
        expected = [
            "zerdatime",
            "Displayed %s/.GeckoViewActivity" % self.appname
        ]
        # wait up to 60 seconds for startup
        for wait_time in xrange(60):
            time.sleep(1)
            self.logcat = self.dm.getLogcat()
            for line in self.logcat:
                for e in expected:
                    if e in line:
                        self.log.debug(line.strip())
                        expected.remove(e)
            if len(expected) == 0:
                return (True, "All expected logcat messages found")
        for e in expected:
            self.log.error("missing from logcat: '%s'" % e)
        return (False, "'%s' not found in logcat" % expected[0])

    def run_tests(self):
        """
           Run simple tests to verify that the geckoview_example app starts.
        """
        all_tests = [self.installed, self.start, self.started]
        self.log.suite_start(all_tests)
        pass_count = 0
        fail_count = 0
        for test in all_tests:
            self.test_name = test.__doc__.strip()
            self.log.test_start(self.test_name)

            expected = 'PASS'
            (passed, message) = test()
            if passed:
                pass_count = pass_count + 1
            else:
                fail_count = fail_count + 1
            status = 'PASS' if passed else 'FAIL'

            self.log.test_end(self.test_name, status, expected, message)

        crashed = self.check_for_crashes()
        if crashed:
            fail_count = 1
        else:
            self.log.info("Passed: %d" % pass_count)
            self.log.info("Failed: %d" % fail_count)
        self.log.suite_end()

        return 1 if fail_count else 0

    def check_for_crashes(self):
        if self.logcat:
            if mozcrash.check_for_java_exception(self.logcat, self.test_name):
                return True
        symbols_path = self.options.symbols_path
        try:
            dump_dir = tempfile.mkdtemp()
            remote_dir = posixpath.join(self.remote_profile, 'minidumps')
            crash_dir_found = False
            # wait up to 60 seconds for gecko startup to progress through
            # crashreporter initialization, in case all tests finished quickly
            for wait_time in xrange(60):
                time.sleep(1)
                if self.dm.dirExists(remote_dir):
                    crash_dir_found = True
                    break
            if not crash_dir_found:
                # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the
                # minidumps directory is automatically created when the app
                # (first) starts, so its lack of presence is a hint that
                # something went wrong.
                print "Automation Error: No crash directory (%s) found on remote device" % \
                    remote_dir
                # Whilst no crash was found, the run should still display as a failure
                return True
            self.dm.getDirectory(remote_dir, dump_dir)
            crashed = mozcrash.log_crashes(self.log, dump_dir, symbols_path, test=self.test_name)
        finally:
            try:
                shutil.rmtree(dump_dir)
            except Exception:
                self.log.warn("unable to remove directory: %s" % dump_dir)
        return crashed

    def cleanup(self):
        """
           Cleanup at end of job run.
        """
        self.log.debug("Cleaning up...")
        self.dm.stopApplication(self.appname)
        self.dm.removeDir(self.remote_profile)
        self.log.debug("Cleanup complete.")


def run_test_harness(log, parser, options):
    device_args = {'deviceRoot': options.remoteTestRoot}
    device_args['adbPath'] = options.adbPath
    if options.deviceIP:
        device_args['host'] = options.deviceIP
        device_args['port'] = options.devicePort
    elif options.deviceSerial:
        device_args['deviceSerial'] = options.deviceSerial
    device_args['packageName'] = options.app
    dm = mozdevice.DroidADB(**device_args)

    runner = GeckoviewTestRunner(log, dm, options)
    result = -1
    try:
        result = runner.run_tests()
    except KeyboardInterrupt:
        log.info("rungeckoview.py | Received keyboard interrupt")
        result = -1
    except Exception:
        traceback.print_exc()
        log.error(
            "rungeckoview.py | Received unexpected exception while running tests")
        result = 1
    finally:
        try:
            runner.cleanup()
        except mozdevice.DMError:
            # ignore device error while cleaning up
            pass
    return result


def main(args=sys.argv[1:]):
    parser = GeckoviewOptions()
    mozlog.commandline.add_logging_group(parser)
    options, args = parser.parse_args()
    if args:
        print >>sys.stderr, """Usage: %s""" % sys.argv[0]
        sys.exit(1)
    log = mozlog.commandline.setup_logging("rungeckoview", options,
                                           {"tbpl": sys.stdout})
    return run_test_harness(log, parser, options)


if __name__ == "__main__":
    sys.exit(main())
