"""The actual script that runs the entire CP test suite.

There is a library of helper functions for the CherryPy test suite,
called "helper.py" (in this folder); this module calls that as a library.
"""

# GREAT CARE has been taken to separate this module from helper.py,
# because different consumers of each have mutually-exclusive import
# requirements. So don't go moving functions from here into helper.py,
# or vice-versa, unless you *really* know what you're doing.


import getopt
from httplib import HTTPSConnection
import os
localDir = os.path.dirname(__file__)
serverpem = os.path.join(os.getcwd(), localDir, 'test.pem')
import sys
import warnings

from cherrypy.lib import profiler


class TestHarness(object):
    """A test harness for the CherryPy framework and CherryPy applications."""
    
    def __init__(self, supervisor, tests, interactive=True):
        """Constructor to populate the TestHarness instance.
        
        tests should be a list of module names (strings).
        """
        self.supervisor = supervisor
        self.tests = tests
        self.interactive = interactive
    
    def run(self, conf=None):
        """Run the test harness (using the given [global] conf)."""
        import cherrypy
        v = sys.version.split()[0]
        print("Python version used to run this test script: %s" % v)
        print("CherryPy version: %s" % cherrypy.__version__)
        if self.supervisor.scheme == "https":
            ssl = " (ssl)"
        else:
            ssl = ""
        print("HTTP server version: %s%s" % (self.supervisor.protocol, ssl))
        print("PID: %s" % os.getpid())
        print("")
        
        cherrypy.server.using_apache = self.supervisor.using_apache
        cherrypy.server.using_wsgi = self.supervisor.using_wsgi
        
        if isinstance(conf, basestring):
            parser = cherrypy.config._Parser()
            conf = parser.dict_from_file(conf).get('global', {})
        else:
            conf = conf or {}
        baseconf = conf.copy()
        baseconf.update({'server.socket_host': self.supervisor.host,
                         'server.socket_port': self.supervisor.port,
                         'server.protocol_version': self.supervisor.protocol,
                         'environment': "test_suite",
                         })
        if self.supervisor.scheme == "https":
            baseconf['server.ssl_certificate'] = serverpem
            baseconf['server.ssl_private_key'] = serverpem
        
        # helper must be imported lazily so the coverage tool
        # can run against module-level statements within cherrypy.
        # Also, we have to do "from cherrypy.test import helper",
        # exactly like each test module does, because a relative import
        # would stick a second instance of webtest in sys.modules,
        # and we wouldn't be able to globally override the port anymore.
        from cherrypy.test import helper, webtest
        webtest.WebCase.interactive = self.interactive
        if self.supervisor.scheme == "https":
            webtest.WebCase.HTTP_CONN = HTTPSConnection
        print("")
        print("Running tests: %s" % self.supervisor)
        
        return helper.run_test_suite(self.tests, baseconf, self.supervisor)


class Supervisor(object):
    """Base class for modeling and controlling servers during testing."""
    
    def __init__(self, **kwargs):
        for k, v in kwargs.iteritems():
            setattr(self, k, v)


class LocalSupervisor(Supervisor):
    """Base class for modeling/controlling servers which run in the same process.
    
    When the server side runs in a different process, start/stop can dump all
    state between each test module easily. When the server side runs in the
    same process as the client, however, we have to do a bit more work to ensure
    config and mounted apps are reset between tests.
    """
    
    using_apache = False
    using_wsgi = False
    
    def __init__(self, **kwargs):
        for k, v in kwargs.iteritems():
            setattr(self, k, v)
        
        import cherrypy
        cherrypy.server.httpserver = self.httpserver_class
        
        engine = cherrypy.engine
        if hasattr(engine, "signal_handler"):
            engine.signal_handler.subscribe()
        if hasattr(engine, "console_control_handler"):
            engine.console_control_handler.subscribe()
        #engine.subscribe('log', lambda msg, level: sys.stderr.write(msg + os.linesep))
    
    def start(self, modulename=None):
        """Load and start the HTTP server."""
        import cherrypy
        
        if modulename:
            # Replace the Tree wholesale.
            cherrypy.tree = cherrypy._cptree.Tree()
            
            # Unhook httpserver so cherrypy.server.start() creates a new
            # one (with config from setup_server, if declared).
            cherrypy.server.httpserver = None
            cherrypy.tree = cherrypy._cptree.Tree()
            
            if '.' in modulename:
                package, test_name = modulename.rsplit('.', 1)
                p = __import__(package, globals(), locals(), fromlist=[test_name])
                m = getattr(p, test_name)
            else:
                m = __import__(modulename, globals(), locals())
            setup = getattr(m, "setup_server", None)
            if setup:
                setup()
            self.teardown = getattr(m, "teardown_server", None)
        
        cherrypy.engine.start()
        
        self.sync_apps()
    
    def sync_apps(self):
        """Tell the server about any apps which the setup functions mounted."""
        pass
    
    def stop(self):
        if self.teardown:
            self.teardown()
        import cherrypy
        cherrypy.engine.exit()


class NativeServerSupervisor(LocalSupervisor):
    """Server supervisor for the builtin HTTP server."""
    
    httpserver_class = "cherrypy._cpnative_server.CPHTTPServer"
    using_apache = False
    using_wsgi = False
    
    def __str__(self):
        return "Builtin HTTP Server on %s:%s" % (self.host, self.port)


class LocalWSGISupervisor(LocalSupervisor):
    """Server supervisor for the builtin WSGI server."""
    
    httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer"
    using_apache = False
    using_wsgi = True
    
    def __str__(self):
        return "Builtin WSGI Server on %s:%s" % (self.host, self.port)
    
    def sync_apps(self):
        """Hook a new WSGI app into the origin server."""
        import cherrypy
        cherrypy.server.httpserver.wsgi_app = self.get_app()
    
    def get_app(self):
        """Obtain a new (decorated) WSGI app to hook into the origin server."""
        import cherrypy
        app = cherrypy.tree
        if self.profile:
            app = profiler.make_app(app, aggregate=False)
        if self.conquer:
            try:
                import wsgiconq
            except ImportError:
                warnings.warn("Error importing wsgiconq. pyconquer will not run.")
            else:
                app = wsgiconq.WSGILogger(app, c_calls=True)
        if self.validate:
            try:
                from wsgiref import validate
            except ImportError:
                warnings.warn("Error importing wsgiref. The validator will not run.")
            else:
                app = validate.validator(app)
        
        return app


def get_cpmodpy_supervisor(**options):
    from cherrypy.test import modpy
    sup = modpy.ModPythonSupervisor(**options)
    sup.template = modpy.conf_cpmodpy
    return sup

def get_modpygw_supervisor(**options):
    from cherrypy.test import modpy
    sup = modpy.ModPythonSupervisor(**options)
    sup.template = modpy.conf_modpython_gateway
    sup.using_wsgi = True
    return sup

def get_modwsgi_supervisor(**options):
    from cherrypy.test import modwsgi
    return modwsgi.ModWSGISupervisor(**options)

def get_modfcgid_supervisor(**options):
    from cherrypy.test import modfcgid
    return modfcgid.ModFCGISupervisor(**options)

def get_wsgi_u_supervisor(**options):
    import cherrypy
    cherrypy.server.wsgi_version = ('u', 0)
    return LocalWSGISupervisor(**options)


class CommandLineParser(object):
    available_servers = {'wsgi': LocalWSGISupervisor,
                         'wsgi_u': get_wsgi_u_supervisor,
                         'native': NativeServerSupervisor,
                         'cpmodpy': get_cpmodpy_supervisor,
                         'modpygw': get_modpygw_supervisor,
                         'modwsgi': get_modwsgi_supervisor,
                         'modfcgid': get_modfcgid_supervisor,
                         }
    default_server = "wsgi"
    
    supervisor_factory = None
    supervisor_options = {
        'scheme': 'http',
        'protocol': "HTTP/1.1",
        'port': 8080,
        'host': '127.0.0.1',
        'profile': False,
        'validate': False,
        'conquer': False,
        }
    
    cover = False
    basedir = None
    interactive = True
    
    shortopts = []
    longopts = ['cover', 'profile', 'validate', 'conquer', 'dumb', '1.0',
                'ssl', 'help', 'basedir=', 'port=', 'server=', 'host=']
    
    def __init__(self, available_tests, args=sys.argv[1:]):
        """Constructor to populate the TestHarness instance.
        
        available_tests should be a list of module names (strings).
        
        args defaults to sys.argv[1:], but you can provide a different
            set of args if you like.
        """
        self.available_tests = available_tests
        self.supervisor_options = self.supervisor_options.copy()
        
        longopts = self.longopts[:]
        longopts.extend(self.available_tests)
        try:
            opts, args = getopt.getopt(args, self.shortopts, longopts)
        except getopt.GetoptError:
            # print help information and exit
            self.help()
            sys.exit(2)
        
        self.tests = []
        
        for o, a in opts:
            if o == '--help':
                self.help()
                sys.exit()
            elif o == "--cover":
                self.cover = True
            elif o == "--profile":
                self.supervisor_options['profile'] = True
            elif o == "--validate":
                self.supervisor_options['validate'] = True
            elif o == "--conquer":
                self.supervisor_options['conquer'] = True
            elif o == "--dumb":
                self.interactive = False
            elif o == "--1.0":
                self.supervisor_options['protocol'] = "HTTP/1.0"
            elif o == "--ssl":
                self.supervisor_options['scheme'] = "https"
            elif o == "--basedir":
                self.basedir = a
            elif o == "--port":
                self.supervisor_options['port'] = int(a)
            elif o == "--host":
                self.supervisor_options['host'] = a
            elif o == "--server":
                if a not in self.available_servers:
                    print('Error: The --server argument must be one of %s.' %
                          '|'.join(self.available_servers.keys()))
                    sys.exit(2)
                self.supervisor_factory = self.available_servers[a]
            else:
                o = o[2:]
                if o in self.available_tests and o not in self.tests:
                    self.tests.append(o)
        
        import cherrypy
        if self.cover and self.supervisor_options['profile']:
            # Print error message and exit
            print('Error: you cannot run the profiler and the '
                   'coverage tool at the same time.')
            sys.exit(2)
        
        if not self.supervisor_factory:
            self.supervisor_factory = self.available_servers[self.default_server]
        
        if not self.tests:
            self.tests = self.available_tests[:]
    
    def help(self):
        """Print help for test.py command-line options."""
        
        import cherrypy
        print("""CherryPy Test Program
    Usage:
        test.py --help --server=* --host=%s --port=%s --1.0 --ssl --cover
            --basedir=path --profile --validate --conquer --dumb --tests**
    """ % (self.__class__.supervisor_options['host'],
           self.__class__.supervisor_options['port']))
        print('    * servers:')
        for name in self.available_servers:
            if name == self.default_server:
                print('        --server=%s (default)' % name)
            else:
                print('        --server=%s' % name)
        
        print("""
    --host=<name or IP addr>: use a host other than the default (%s).
        Not yet available with mod_python servers.
    --port=<int>: use a port other than the default (%s).
    --1.0: use HTTP/1.0 servers instead of default HTTP/1.1.
    
    --cover: turn on code-coverage tool.
    --basedir=path: display coverage stats for some path other than cherrypy.
    
    --profile: turn on WSGI profiling tool.
    --validate: use wsgiref.validate (builtin in Python 2.5).
    --conquer: use wsgiconq (which uses pyconquer) to trace calls.
    --dumb: turn off the interactive output features.
    """ % (self.__class__.supervisor_options['host'],
           self.__class__.supervisor_options['port']))
        
        print('    ** tests:')
        for name in self.available_tests:
            print('        --' + name)
    
    def start_coverage(self):
        """Start the coverage tool.
        
        To use this feature, you need to download 'coverage.py',
        either Gareth Rees' original implementation:
        http://www.garethrees.org/2001/12/04/python-coverage/
        
        or Ned Batchelder's enhanced version:
        http://www.nedbatchelder.com/code/modules/coverage.html
        
        If neither module is found in PYTHONPATH,
        coverage is silently(!) disabled.
        """
        try:
            from coverage import the_coverage as coverage
            c = os.path.join(os.path.dirname(__file__), "../lib/coverage.cache")
            coverage.cache_default = c
            if c and os.path.exists(c):
                os.remove(c)
            coverage.start()
            import cherrypy
            from cherrypy.lib import covercp
            cherrypy.engine.subscribe('start', covercp.start)
        except ImportError:
            coverage = None
        self.coverage = coverage
    
    def stop_coverage(self):
        """Stop the coverage tool, save results, and report."""
        import cherrypy
        from cherrypy.lib import covercp
        cherrypy.engine.unsubscribe('start', covercp.start)
        if self.coverage:
            self.coverage.save()
            self.report_coverage()
            print("run cherrypy/lib/covercp.py as a script to serve "
                   "coverage results on port 8080")
    
    def report_coverage(self):
        """Print a summary from the code coverage tool."""
        
        import cherrypy
        basedir = self.basedir
        if basedir is None:
            # Assume we want to cover everything in "../../cherrypy/"
            basedir = os.path.normpath(os.path.join(os.getcwd(), localDir, '../'))
        else:
            if not os.path.isabs(basedir):
                basedir = os.path.normpath(os.path.join(os.getcwd(), basedir))
        basedir = basedir.lower()
        
        self.coverage.get_ready()
        morfs = [x for x in self.coverage.cexecuted
                 if x.lower().startswith(basedir)]
        
        total_statements = 0
        total_executed = 0
        
        print("")
        sys.stdout.write("CODE COVERAGE (this might take a while)")
        for morf in morfs:
            sys.stdout.write(".")
            sys.stdout.flush()
##            name = os.path.split(morf)[1]
            if morf.find('test') != -1:
                continue
            try:
                _, statements, _, missing, readable  = self.coverage.analysis2(morf)
                n = len(statements)
                m = n - len(missing)
                total_statements = total_statements + n
                total_executed = total_executed + m
            except KeyboardInterrupt:
                raise
            except:
                # No, really! We truly want to ignore any other errors.
                pass
        
        pc = 100.0
        if total_statements > 0:
            pc = 100.0 * total_executed / total_statements
        
        print("\nTotal: %s Covered: %s Percent: %2d%%"
               % (total_statements, total_executed, pc))
    
    def run(self, conf=None):
        """Run the test harness (using the given [global] conf)."""
        conf = conf or {}
        
        # Start the coverage tool before importing cherrypy,
        # so module-level global statements are covered.
        if self.cover:
            self.start_coverage()
        
        supervisor = self.supervisor_factory(**self.supervisor_options)
        
        if supervisor.using_apache and 'test_conn' in self.tests:
            self.tests.remove('test_conn')
        
        h = TestHarness(supervisor, self.tests, self.interactive)
        success = h.run(conf)
        
        if self.supervisor_options['profile']:
            print("")
            print("run /cherrypy/lib/profiler.py as a script to serve "
                   "profiling results on port 8080")
        
        if self.cover:
            self.stop_coverage()
        
        return success


def prefer_parent_path():
    # Place this __file__'s grandparent (../../) at the start of sys.path,
    # so that all cherrypy/* imports are from this __file__'s package.
    curpath = os.path.normpath(os.path.join(os.getcwd(), localDir))
    grandparent = os.path.normpath(os.path.join(curpath, '../../'))
    if grandparent not in sys.path:
        sys.path.insert(0, grandparent)

def run():
    
    prefer_parent_path()
    
    testList = [
        'test_auth_basic',
        'test_auth_digest',
        'test_bus',
        'test_proxy',
        'test_caching',
        'test_config',
        'test_conn',
        'test_core',
        'test_tools',
        'test_encoding',
        'test_etags',
        'test_http',
        'test_httpauth',
        'test_httplib',
        'test_json',
        'test_logging',
        'test_objectmapping',
        'test_dynamicobjectmapping',
        'test_misc_tools',
        'test_request_obj',
        'test_static',
        'test_tutorials',
        'test_virtualhost',
        'test_mime',
        'test_session',
        'test_sessionauthenticate',
        'test_states',
        'test_config_server',
        'test_xmlrpc',
        'test_wsgiapps',
        'test_wsgi_ns',
        'test_wsgi_vhost',
        
        # Run refleak test as late as possible to
        # catch refleaks from all exercised tests.
        'test_refleaks',
    ]
    
    try:
        import routes
        testList.append('test_routes')
    except ImportError:
        pass
    
    clp = CommandLineParser(testList)
    success = clp.run()
    import cherrypy
    if clp.interactive:
        print("")
        raw_input('hit enter')
    sys.exit(success)


if __name__ == '__main__':
    run()