########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Lib/TestSuite/TestCoverage.py,v 1.9 2005/04/07 20:05:52 jkloth Exp $
"""
A class that uses Python's profiler to help TestModule know which
functions have been called in a particular module.

Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import sys, os, inspect

class TestCoverage:

    def __init__(self, moduleName, ignored=None):
        self.module = moduleName
        module = __import__(moduleName, {}, {}, ['*'])
        self.path = os.path.dirname(module.__file__)
        self.ignored = ignored or []
        self.data = {}
        return

    def _getFunctionList(self):
        data = {}
        modules = {None : None}
        for entry in os.listdir(self.path):
            module_name = inspect.getmodulename(entry)
            if modules.has_key(module_name):
                # Don't process duplicates
                continue

            try:
                module = __import__(self.module + '.' + module_name, {}, {}, '*')
            except (ImportError, NameError):
                # It probably needs something else to be imported first,
                # but we have no easy way of knowing what, so we have to skip it.
                continue

            for top_name,object in vars(module).items():
                if object in self.ignored: continue
                if inspect.isfunction(object) and top_name != '_':
                    data[object.func_code] = object
                elif inspect.isclass(object):
                    # Get the methods defined on this class
                    for name, method in inspect.getmembers(object,
                                                           inspect.ismethod):
                        if method in self.ignored: continue
                        # Make sure this method is not a C function
                        if name not in ['__str__', '__repr__','pprint'] and \
                           inspect.isfunction(method.im_func):
                            data[method.im_func.func_code] = method

        # Only watch the objects that are defined directly in this package
        self.data = {}
        for code, object in data.items():
            if self.path == os.path.dirname(code.co_filename):
                self.data[code] = object
        return

    def _dispatch(self, frame, event, arg):
        if event == 'call':
            fcode = frame.f_code
            # Any items remaining in self.data will be those not called
            if self.data.has_key(fcode):
                del self.data[fcode]
        return 1

    def _start(self, tester):
        self._getFunctionList()
        sys.setprofile(self._dispatch)
        return

    def _end(self, tester):
        # Stop watching function calls
        sys.setprofile(None)

        modules = {}
        for code, object in self.data.items():
            if inspect.ismethod(object):
                type = 'method'
                name = object.im_class.__name__ + '.' + object.__name__
                module = object.im_class.__module__
            else:
                type = 'function'
                name = object.__name__
                module = object.func_globals['__name__']
            if not modules.has_key(module):
                modules[module] = {'method' : [],
                                   'function' : [],
                                   }
            modules[module][type].append((name, code.co_firstlineno))

        tester.startGroup('Coverage Test')
        tester.startTest("Verifing called functions")

        keys = modules.keys()
        keys.sort()
        for module in keys:
            lines = []
            if modules[module]['function']:
                funcs = modules[module]['function']
                lines.append('Functions not called in %r:' % module)
                funcs.sort()
                for name, line in funcs:
                    lines.append('  %r on line %s' % (name, line))
            if modules[module]['method']:
                lines.append('Methods not called in %r:' % module)
                meths = modules[module]['method']
                meths.sort()
                for name, line in meths:
                    lines.append('  %r on line %s' % (name, line))
            tester.warning('\n'.join(lines))
        tester.testDone()
        tester.groupDone()
        return

