testing/xpcshell/remotexpcshelltests.py
author B2G Bumper Bot <release+b2gbumper@mozilla.com>
Tue, 26 Aug 2014 08:05:59 -0700
changeset 224446 df1112296179c3ab7eb5188f86b011e54b072606
parent 221633 04500acd1bb79d5af4ae2e119fa1c51acf6a02e1
child 238913 b2dc4f1dc9b38b9de7dbf3cad503f31dce50b04c
permissions -rw-r--r--
Bumping manifests a=b2g-bump

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

import posixpath
import sys, os
import subprocess
import runxpcshelltests as xpcshell
import tempfile
from automationutils import replaceBackSlashes
from zipfile import ZipFile
import shutil
import mozdevice
import mozfile
import mozinfo

here = os.path.dirname(os.path.abspath(__file__))

def remoteJoin(path1, path2):
    return posixpath.join(path1, path2)

class RemoteXPCShellTestThread(xpcshell.XPCShellTestThread):
    def __init__(self, *args, **kwargs):
        xpcshell.XPCShellTestThread.__init__(self, *args, **kwargs)

        # embed the mobile params from the harness into the TestThread
        mobileArgs = kwargs.get('mobileArgs')
        for key in mobileArgs:
            setattr(self, key, mobileArgs[key])

    def buildCmdTestFile(self, name):
        remoteDir = self.remoteForLocal(os.path.dirname(name))
        if remoteDir == self.remoteHere:
            remoteName = os.path.basename(name)
        else:
            remoteName = remoteJoin(remoteDir, os.path.basename(name))
        return ['-e', 'const _TEST_FILE = ["%s"];' %
                 replaceBackSlashes(remoteName)]

    def remoteForLocal(self, local):
        for mapping in self.pathMapping:
            if (os.path.abspath(mapping.local) == os.path.abspath(local)):
                return mapping.remote
        return local


    def setupTempDir(self):
        # make sure the temp dir exists
        if not self.device.dirExists(self.remoteTmpDir):
            self.device.mkDir(self.remoteTmpDir)
        # env var is set in buildEnvironment
        return self.remoteTmpDir

    def setupPluginsDir(self):
        if not os.path.isdir(self.pluginsPath):
            return None

        # making sure tmp dir is set up
        self.setupTempDir()

        pluginsDir = remoteJoin(self.remoteTmpDir, "plugins")
        self.device.pushDir(self.pluginsPath, pluginsDir)
        if self.interactive:
            self.log.info("TEST-INFO | plugins dir is %s" % pluginsDir)
        return pluginsDir

    def setupProfileDir(self):
        self.device.removeDir(self.profileDir)
        self.device.mkDir(self.profileDir)
        if self.interactive or self.singleFile:
            self.log.info("TEST-INFO | profile dir is %s" % self.profileDir)
        return self.profileDir

    def logCommand(self, name, completeCmd, testdir):
        self.log.info("TEST-INFO | %s | full command: %r" % (name, completeCmd))
        self.log.info("TEST-INFO | %s | current directory: %r" % (name, self.remoteHere))
        self.log.info("TEST-INFO | %s | environment: %s" % (name, self.env))

    def getHeadAndTailFiles(self, test):
        """Override parent method to find files on remote device."""
        def sanitize_list(s, kind):
            for f in s.strip().split(' '):
                f = f.strip()
                if len(f) < 1:
                    continue

                path = remoteJoin(self.remoteHere, f)
                if not self.device.fileExists(path):
                    raise Exception('%s file does not exist: %s' % ( kind,
                        path))

                yield path

        self.remoteHere = self.remoteForLocal(test['here'])

        return (list(sanitize_list(test['head'], 'head')),
                list(sanitize_list(test['tail'], 'tail')))

    def buildXpcsCmd(self, testdir):
        # change base class' paths to remote paths and use base class to build command
        self.xpcshell = remoteJoin(self.remoteBinDir, "xpcw")
        self.headJSPath = remoteJoin(self.remoteScriptsDir, 'head.js')
        self.httpdJSPath = remoteJoin(self.remoteComponentsDir, 'httpd.js')
        self.httpdManifest = remoteJoin(self.remoteComponentsDir, 'httpd.manifest')
        self.testingModulesDir = self.remoteModulesDir
        self.testharnessdir = self.remoteScriptsDir
        xpcshell.XPCShellTestThread.buildXpcsCmd(self, testdir)
        # remove "-g <dir> -a <dir>" and add "--greomni <apk>"
        del(self.xpcsCmd[1:5])
        if self.options.localAPK:
            self.xpcsCmd.insert(3, '--greomni')
            self.xpcsCmd.insert(4, self.remoteAPK)

        if self.remoteDebugger:
            # for example, "/data/local/gdbserver" "localhost:12345"
            self.xpcsCmd = [
              self.remoteDebugger,
              self.remoteDebuggerArgs,
              self.xpcsCmd]

    def testTimeout(self, test_file, proc):
        self.timedout = True
        if not self.retry:
            self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file)
        self.kill(proc)

    def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None):
        self.timedout = False
        cmd.insert(1, self.remoteHere)
        outputFile = "xpcshelloutput"
        with open(outputFile, 'w+') as f:
            try:
                self.shellReturnCode = self.device.shell(cmd, f, timeout=timeout+10)
            except mozdevice.DMError as e:
                if self.timedout:
                    # If the test timed out, there is a good chance the SUTagent also
                    # timed out and failed to return a return code, generating a
                    # DMError. Ignore the DMError to simplify the error report.
                    self.shellReturnCode = None
                    pass
                else:
                    raise e
        # The device manager may have timed out waiting for xpcshell.
        # Guard against an accumulation of hung processes by killing
        # them here. Note also that IPC tests may spawn new instances
        # of xpcshell.
        self.device.killProcess(cmd[0])
        self.device.killProcess("xpcshell")
        return outputFile

    def checkForCrashes(self,
                        dump_directory,
                        symbols_path,
                        test_name=None):
        if not self.device.dirExists(self.remoteMinidumpDir):
            # The minidumps directory is automatically created when Fennec
            # (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" % self.remoteMinidumpDir
            # Whilst no crash was found, the run should still display as a failure
            return True
        with mozfile.TemporaryDirectory() as dumpDir:
            self.device.getDirectory(self.remoteMinidumpDir, dumpDir)
            crashed = xpcshell.XPCShellTestThread.checkForCrashes(self, dumpDir, symbols_path, test_name)
            self.device.removeDir(self.remoteMinidumpDir)
            self.device.mkDir(self.remoteMinidumpDir)
        return crashed

    def communicate(self, proc):
        f = open(proc, "r")
        contents = f.read()
        f.close()
        os.remove(proc)
        return contents, ""

    def poll(self, proc):
        if self.device.processExist("xpcshell") is None:
            return self.getReturnCode(proc)
        # Process is still running
        return None

    def kill(self, proc):
        return self.device.killProcess("xpcshell", True)

    def getReturnCode(self, proc):
        if self.shellReturnCode is not None:
            return self.shellReturnCode
        else:
            return -1

    def removeDir(self, dirname):
        self.device.removeDir(dirname)

    #TODO: consider creating a separate log dir.  We don't have the test file structure,
    #      so we use filename.log.  Would rather see ./logs/filename.log
    def createLogFile(self, test, stdout):
        try:
            f = None
            filename = test.replace('\\', '/').split('/')[-1] + ".log"
            f = open(filename, "w")
            f.write(stdout)

        finally:
            if f is not None:
                f.close()


# A specialization of XPCShellTests that runs tests on an Android device
# via devicemanager.
class XPCShellRemote(xpcshell.XPCShellTests, object):

    def __init__(self, devmgr, options, args, log=None):
        xpcshell.XPCShellTests.__init__(self, log)

        # Add Android version (SDK level) to mozinfo so that manifest entries
        # can be conditional on android_version.
        androidVersion = devmgr.shellCheckOutput(['getprop', 'ro.build.version.sdk'])
        mozinfo.info['android_version'] = androidVersion

        self.localLib = options.localLib
        self.localBin = options.localBin
        self.options = options
        self.device = devmgr
        self.pathMapping = []
        self.remoteTestRoot = "%s/xpcshell" % self.device.deviceRoot
        # remoteBinDir contains xpcshell and its wrapper script, both of which must
        # be executable. Since +x permissions cannot usually be set on /mnt/sdcard,
        # and the test root may be on /mnt/sdcard, remoteBinDir is set to be on
        # /data/local, always.
        self.remoteBinDir = "/data/local/xpcb"
        # Terse directory names are used here ("c" for the components directory)
        # to minimize the length of the command line used to execute
        # xpcshell on the remote device. adb has a limit to the number
        # of characters used in a shell command, and the xpcshell command
        # line can be quite complex.
        self.remoteTmpDir = remoteJoin(self.remoteTestRoot, "tmp")
        self.remoteScriptsDir = self.remoteTestRoot
        self.remoteComponentsDir = remoteJoin(self.remoteTestRoot, "c")
        self.remoteModulesDir = remoteJoin(self.remoteTestRoot, "m")
        self.remoteMinidumpDir = remoteJoin(self.remoteTestRoot, "minidumps")
        self.profileDir = remoteJoin(self.remoteTestRoot, "p")
        self.remoteDebugger = options.debugger
        self.remoteDebuggerArgs = options.debuggerArgs
        self.testingModulesDir = options.testingModulesDir

        self.env = {}

        if self.options.objdir:
            self.xpcDir = os.path.join(self.options.objdir, "_tests/xpcshell")
        elif os.path.isdir(os.path.join(here, 'tests')):
            self.xpcDir = os.path.join(here, 'tests')
        else:
            print >> sys.stderr, "Couldn't find local xpcshell test directory"
            sys.exit(1)

        if options.localAPK:
            self.localAPKContents = ZipFile(options.localAPK)
        if options.setup:
            self.setupUtilities()
            self.setupModules()
            self.setupTestDir()
        self.setupMinidumpDir()
        self.remoteAPK = None
        if options.localAPK:
            self.remoteAPK = remoteJoin(self.remoteBinDir, os.path.basename(options.localAPK))
            self.setAppRoot()

        # data that needs to be passed to the RemoteXPCShellTestThread
        self.mobileArgs = {
            'device': self.device,
            'remoteBinDir': self.remoteBinDir,
            'remoteScriptsDir': self.remoteScriptsDir,
            'remoteComponentsDir': self.remoteComponentsDir,
            'remoteModulesDir': self.remoteModulesDir,
            'options': self.options,
            'remoteDebugger': self.remoteDebugger,
            'pathMapping': self.pathMapping,
            'profileDir': self.profileDir,
            'remoteTmpDir': self.remoteTmpDir,
            'remoteMinidumpDir': self.remoteMinidumpDir,
        }
        if self.remoteAPK:
            self.mobileArgs['remoteAPK'] = self.remoteAPK

    def setLD_LIBRARY_PATH(self):
        self.env["LD_LIBRARY_PATH"] = self.remoteBinDir

    def pushWrapper(self):
        # Rather than executing xpcshell directly, this wrapper script is
        # used. By setting environment variables and the cwd in the script,
        # the length of the per-test command line is shortened. This is
        # often important when using ADB, as there is a limit to the length
        # of the ADB command line.
        localWrapper = tempfile.mktemp()
        f = open(localWrapper, "w")
        f.write("#!/system/bin/sh\n")
        for envkey, envval in self.env.iteritems():
            f.write("export %s=%s\n" % (envkey, envval))
        f.write("cd $1\n")
        f.write("echo xpcw: cd $1\n")
        f.write("shift\n")
        f.write("echo xpcw: xpcshell \"$@\"\n")
        f.write("%s/xpcshell \"$@\"\n" % self.remoteBinDir)
        f.close()
        remoteWrapper = remoteJoin(self.remoteBinDir, "xpcw")
        self.device.pushFile(localWrapper, remoteWrapper)
        os.remove(localWrapper)
        self.device.chmodDir(self.remoteBinDir)

    def buildEnvironment(self):
        self.buildCoreEnvironment()
        self.setLD_LIBRARY_PATH()
        self.env["MOZ_LINKER_CACHE"] = self.remoteBinDir
        if self.options.localAPK and self.appRoot:
            self.env["GRE_HOME"] = self.appRoot
        self.env["XPCSHELL_TEST_PROFILE_DIR"] = self.profileDir
        self.env["TMPDIR"] = self.remoteTmpDir
        self.env["HOME"] = self.profileDir
        self.env["XPCSHELL_TEST_TEMP_DIR"] = self.remoteTmpDir
        self.env["XPCSHELL_MINIDUMP_DIR"] = self.remoteMinidumpDir
        if self.options.setup:
            self.pushWrapper()

    def setAppRoot(self):
        # Determine the application root directory associated with the package
        # name used by the Fennec APK.
        self.appRoot = None
        packageName = None
        if self.options.localAPK:
            try:
                packageName = self.localAPKContents.read("package-name.txt")
                if packageName:
                    self.appRoot = self.device.getAppRoot(packageName.strip())
            except Exception as detail:
                print "unable to determine app root: " + str(detail)
                pass
        return None

    def setupUtilities(self):
        if (not self.device.dirExists(self.remoteBinDir)):
            # device.mkDir may fail here where shellCheckOutput may succeed -- see bug 817235
            try:
                self.device.shellCheckOutput(["mkdir", self.remoteBinDir]);
            except mozdevice.DMError:
                # Might get a permission error; try again as root, if available
                self.device.shellCheckOutput(["mkdir", self.remoteBinDir], root=True);
                self.device.shellCheckOutput(["chmod", "777", self.remoteBinDir], root=True);

        remotePrefDir = remoteJoin(self.remoteBinDir, "defaults/pref")
        if (self.device.dirExists(self.remoteTmpDir)):
            self.device.removeDir(self.remoteTmpDir)
        self.device.mkDir(self.remoteTmpDir)
        if (not self.device.dirExists(remotePrefDir)):
            self.device.mkDirs(remoteJoin(remotePrefDir, "extra"))
        if (not self.device.dirExists(self.remoteScriptsDir)):
            self.device.mkDir(self.remoteScriptsDir)
        if (not self.device.dirExists(self.remoteComponentsDir)):
            self.device.mkDir(self.remoteComponentsDir)

        local = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'head.js')
        remoteFile = remoteJoin(self.remoteScriptsDir, "head.js")
        self.device.pushFile(local, remoteFile)

        # The xpcshell binary is required for all tests. Additional binaries
        # are required for some tests. This list should be similar to
        # TEST_HARNESS_BINS in testing/mochitest/Makefile.in.
        binaries = ["xpcshell",
                    "ssltunnel",
                    "certutil",
                    "pk12util",
                    "BadCertServer",
                    "ClientAuthServer",
                    "OCSPStaplingServer",
                    "GenerateOCSPResponse"]
        for fname in binaries:
            local = os.path.join(self.localBin, fname)
            if os.path.isfile(local):
                print >> sys.stderr, "Pushing %s.." % fname
                remoteFile = remoteJoin(self.remoteBinDir, fname)
                self.device.pushFile(local, remoteFile)
            else:
                print >> sys.stderr, "*** Expected binary %s not found in %s!" % (fname, self.localBin)

        local = os.path.join(self.localBin, "components/httpd.js")
        remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.js")
        self.device.pushFile(local, remoteFile)

        local = os.path.join(self.localBin, "components/httpd.manifest")
        remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.manifest")
        self.device.pushFile(local, remoteFile)

        local = os.path.join(self.localBin, "components/test_necko.xpt")
        remoteFile = remoteJoin(self.remoteComponentsDir, "test_necko.xpt")
        self.device.pushFile(local, remoteFile)

        if self.options.localAPK:
            remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(self.options.localAPK))
            self.device.pushFile(self.options.localAPK, remoteFile)

        self.pushLibs()

    def pushLibs(self):
        pushed_libs_count = 0
        if self.options.localAPK:
            try:
                dir = tempfile.mkdtemp()
                szip = os.path.join(self.localBin, '..', 'host', 'bin', 'szip')
                if not os.path.exists(szip):
                    # Tinderbox builds must run szip from the test package
                    szip = os.path.join(self.localBin, 'host', 'szip')
                if not os.path.exists(szip):
                    # If the test package doesn't contain szip, it means files
                    # are not szipped in the test package.
                    szip = None
                for info in self.localAPKContents.infolist():
                    if info.filename.endswith(".so"):
                        print >> sys.stderr, "Pushing %s.." % info.filename
                        remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(info.filename))
                        self.localAPKContents.extract(info, dir)
                        file = os.path.join(dir, info.filename)
                        if szip:
                            out = subprocess.check_output([szip, '-d', file], stderr=subprocess.STDOUT)
                        self.device.pushFile(os.path.join(dir, info.filename), remoteFile)
                        pushed_libs_count += 1
            finally:
                shutil.rmtree(dir)
            return pushed_libs_count

        for file in os.listdir(self.localLib):
            if (file.endswith(".so")):
                print >> sys.stderr, "Pushing %s.." % file
                if 'libxul' in file:
                    print >> sys.stderr, "This is a big file, it could take a while."
                remoteFile = remoteJoin(self.remoteBinDir, file)
                self.device.pushFile(os.path.join(self.localLib, file), remoteFile)
                pushed_libs_count += 1

        # Additional libraries may be found in a sub-directory such as "lib/armeabi-v7a"
        localArmLib = os.path.join(self.localLib, "lib")
        if os.path.exists(localArmLib):
            for root, dirs, files in os.walk(localArmLib):
                for file in files:
                    if (file.endswith(".so")):
                        print >> sys.stderr, "Pushing %s.." % file
                        remoteFile = remoteJoin(self.remoteBinDir, file)
                        self.device.pushFile(os.path.join(root, file), remoteFile)
                        pushed_libs_count += 1

        return pushed_libs_count

    def setupModules(self):
        if self.testingModulesDir:
            self.device.pushDir(self.testingModulesDir, self.remoteModulesDir)

    def setupTestDir(self):
        print 'pushing %s' % self.xpcDir
        try:
            self.device.pushDir(self.xpcDir, self.remoteScriptsDir, retryLimit=10)
        except TypeError:
            # Foopies have an older mozdevice ver without retryLimit
            self.device.pushDir(self.xpcDir, self.remoteScriptsDir)

    def setupMinidumpDir(self):
        if self.device.dirExists(self.remoteMinidumpDir):
            self.device.removeDir(self.remoteMinidumpDir)
        self.device.mkDir(self.remoteMinidumpDir)

    def buildTestList(self):
        xpcshell.XPCShellTests.buildTestList(self)
        uniqueTestPaths = set([])
        for test in self.alltests:
            uniqueTestPaths.add(test['here'])
        for testdir in uniqueTestPaths:
            abbrevTestDir = os.path.relpath(testdir, self.xpcDir)
            remoteScriptDir = remoteJoin(self.remoteScriptsDir, abbrevTestDir)
            self.pathMapping.append(PathMapping(testdir, remoteScriptDir))

class RemoteXPCShellOptions(xpcshell.XPCShellOptions):

    def __init__(self):
        xpcshell.XPCShellOptions.__init__(self)
        defaults = {}

        self.add_option("--deviceIP", action="store",
                        type = "string", dest = "deviceIP",
                        help = "ip address of remote device to test")
        defaults["deviceIP"] = None

        self.add_option("--devicePort", action="store",
                        type = "string", dest = "devicePort",
                        help = "port of remote device to test")
        defaults["devicePort"] = 20701

        self.add_option("--dm_trans", action="store",
                        type = "string", dest = "dm_trans",
                        help = "the transport to use to communicate with device: [adb|sut]; default=sut")
        defaults["dm_trans"] = "sut"

        self.add_option("--objdir", action="store",
                        type = "string", dest = "objdir",
                        help = "local objdir, containing xpcshell binaries")
        defaults["objdir"] = None

        self.add_option("--apk", action="store",
                        type = "string", dest = "localAPK",
                        help = "local path to Fennec APK")
        defaults["localAPK"] = None

        self.add_option("--noSetup", action="store_false",
                        dest = "setup",
                        help = "do not copy any files to device (to be used only if device is already setup)")
        defaults["setup"] = True

        self.add_option("--local-lib-dir", action="store",
                        type = "string", dest = "localLib",
                        help = "local path to library directory")
        defaults["localLib"] = None

        self.add_option("--local-bin-dir", action="store",
                        type = "string", dest = "localBin",
                        help = "local path to bin directory")
        defaults["localBin"] = None

        self.add_option("--remoteTestRoot", action = "store",
                    type = "string", dest = "remoteTestRoot",
                    help = "remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
        defaults["remoteTestRoot"] = None

        self.set_defaults(**defaults)

    def verifyRemoteOptions(self, options):
        if options.localLib is None:
            if options.localAPK and options.objdir:
                for path in ['dist/fennec', 'fennec/lib']:
                    options.localLib = os.path.join(options.objdir, path)
                    if os.path.isdir(options.localLib):
                        break
                else:
                    self.error("Couldn't find local library dir, specify --local-lib-dir")
            elif options.objdir:
                options.localLib = os.path.join(options.objdir, 'dist/bin')
            elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
                # assume tests are being run from a tests.zip
                options.localLib = os.path.abspath(os.path.join(here, '..', 'bin'))
            else:
                self.error("Couldn't find local library dir, specify --local-lib-dir")

        if options.localBin is None:
            if options.objdir:
                for path in ['dist/bin', 'bin']:
                    options.localBin = os.path.join(options.objdir, path)
                    if os.path.isdir(options.localBin):
                        break
                else:
                    self.error("Couldn't find local binary dir, specify --local-bin-dir")
            elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
                # assume tests are being run from a tests.zip
                options.localBin = os.path.abspath(os.path.join(here, '..', 'bin'))
            else:
                self.error("Couldn't find local binary dir, specify --local-bin-dir")
        return options

class PathMapping:

    def __init__(self, localDir, remoteDir):
        self.local = localDir
        self.remote = remoteDir

def main():

    if sys.version_info < (2,7):
        print >>sys.stderr, "Error: You must use python version 2.7 or newer but less than 3.0"
        sys.exit(1)

    parser = RemoteXPCShellOptions()
    options, args = parser.parse_args()
    if not options.localAPK:
        for file in os.listdir(os.path.join(options.objdir, "dist")):
            if (file.endswith(".apk") and file.startswith("fennec")):
                options.localAPK = os.path.join(options.objdir, "dist")
                options.localAPK = os.path.join(options.localAPK, file)
                print >>sys.stderr, "using APK: " + options.localAPK
                break
        else:
            print >>sys.stderr, "Error: please specify an APK"
            sys.exit(1)

    options = parser.verifyRemoteOptions(options)

    if len(args) < 1 and options.manifest is None:
        print >>sys.stderr, """Usage: %s <test dirs>
             or: %s --manifest=test.manifest """ % (sys.argv[0], sys.argv[0])
        sys.exit(1)

    if options.dm_trans == "adb":
        if options.deviceIP:
            dm = mozdevice.DroidADB(options.deviceIP, options.devicePort, packageName=None, deviceRoot=options.remoteTestRoot)
        else:
            dm = mozdevice.DroidADB(packageName=None, deviceRoot=options.remoteTestRoot)
    else:
        if not options.deviceIP:
            print "Error: you must provide a device IP to connect to via the --device option"
            sys.exit(1)
        dm = mozdevice.DroidSUT(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)

    if options.interactive and not options.testPath:
        print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
        sys.exit(1)

    xpcsh = XPCShellRemote(dm, options, args)

    # we don't run concurrent tests on mobile
    options.sequential = True

    if not xpcsh.runTests(xpcshell='xpcshell',
                          testClass=RemoteXPCShellTestThread,
                          testdirs=args[0:],
                          mobileArgs=xpcsh.mobileArgs,
                          **options.__dict__):
        sys.exit(1)


if __name__ == '__main__':
    main()