import sys, traceback, os, cStringIO, time, string
import linecache, types

# Enhanced number manipulation
from Ft.Lib import number

OK = 0
SKIPPED = 1
FAILED = -1

###########################################
#
#  Verbosity Levels
#  4  print out group headers, all test names and test results and debug messges
#  3  print out group headers, all test names and test results
#  2  print out group headers and errors and warnings
#  1  print out group headers and errors
#  0  Nothing
VB_SHOW_DEBUGS = 4
VB_SHOW_TEST_TITLES = 3
VB_SHOW_MESSAGES = 3
VB_SHOW_WARNINGS = 2
VB_SHOW_GROUP_TITLES = 1
VB_SHOW_ERRORS = 1

_group_headers = ['#', '*', '=', ':', '%', '+', '-','@','$','&']

class TestItem:
    def __init__(self, suite, title, group):
        self.suite = suite
        self.title = title
        self.group = group
        self.messages = []
        self.hasErrors = 0
        self.hasWarnings = 0
        self.startTime = time.time()
        self.compareTime = 0

        if suite.useColor:
            #self.moveTo = '\033[%dG' % (suite.columns - 9)
            self.colorSuccess = '\033[1;32m'
            self.colorFailure = '\033[1;31m'
            self.colorWarning = '\033[1;33m'
            self.colorNormal = '\033[0;39m'
        else:
            self.colorSuccess = ''
            self.colorFailure = ''
            self.colorWarning = ''
            self.colorNormal = ''

        self.suite.totalItems = self.suite.totalItems + 1


    def done(self):
        return self.group.testDone()

    def finish(self):

        if self.hasErrors:
            msg = self._failure()
            retVal = FAILED
        elif self.hasWarnings:
            msg = self._skipped()
            retVal = SKIPPED
        else:
            msg = self._success()
            retVal = OK

        totalTime = time.time() - self.startTime
        title = self.title + ' ('
        title = title + '%1.3lf/' % (totalTime - self.compareTime)
        title = title + '%1.3lf secs)' % (totalTime)
        spaces = self.suite.columns - 9
        spaces = spaces - len(title)

        #decide here is we print the title
        if (self.suite.verbose >= VB_SHOW_TEST_TITLES or
            self.suite.verbose >= VB_SHOW_WARNINGS and retVal == SKIPPED or
            self.suite.verbose >= VB_SHOW_ERRORS and retVal == FAILED):
            
        
            print title + ' '*spaces + msg
            

        #Show all messages that were added to the queue
        for msg in self.messages:
            print msg

        self.suite.testTime = self.suite.testTime + totalTime
        self.suite = None
        self.group = None
        return retVal

    def debug(self, msg):
        if self.suite.verbose >= VB_SHOW_DEBUGS:
            self.messages.append(msg)

    def message(self, msg):
        if self.suite.verbose >= VB_SHOW_MESSAGES:
            self.messages.append(msg)

    def warning(self, msg=None):
        if self.suite.verbose >= VB_SHOW_WARNINGS:
            msg and self.messages.append(msg)
            self.hasWarnings = 1

    def error(self, msg=None, saveTrace=0):
        filename = None
        lineno = None
        msgs = []
        msg and msgs.append(msg)
        if saveTrace:
            stream = cStringIO.StringIO()
            traceback.print_exc(None, stream)
            if self.suite.verbose >= VB_SHOW_ERRORS:
                msgs.append(stream.getvalue())

        if filename is None:
            try:
                raise "Foo"
            except:
                tb = sys.exc_info()[2]
                f = tb.tb_frame.f_back.f_back
                lineno = f.f_lineno
		co = f.f_code
		filename = co.co_filename                
                while string.find(filename,"TestSuite.") != -1:
                    f = f.f_back
                    lineno = f.f_lineno
                    co = f.f_code
                    filename = co.co_filename                
                    
		line = linecache.getline(filename, lineno)
                if self.suite.verbose >= VB_SHOW_ERRORS:
                    msgs.insert(0,"Line: %d File %s" % (lineno,filename))
                    msgs.insert(1,"%s" % (line))

        if self.suite.verbose >= VB_SHOW_ERRORS:
            for m in msgs:
                self.messages.append(m)

        self.hasErrors = 1
        if self.suite.stopOnError:
            retval = self.finish()
            sys.exit(retval)

    ### Internal Methods ###
    def _success(self):
        return '[%s  OK  %s]' % (self.colorSuccess, self.colorNormal)

    def _skipped(self):
        return '[%s WARN %s]' % (self.colorWarning, self.colorNormal)

    def _failure(self):
        return '[%sFAILED%s]' % (self.colorFailure, self.colorNormal)


class TestGroup:
    def __init__(self, suite, title=None):
        self.suite = suite
        self.title = title
        self.tests = []
        self.retVal = OK
        #decide here is we print the title
        if title and self.suite.verbose >= VB_SHOW_GROUP_TITLES:
            header = _group_headers[len(suite.groups)]*10
            print '%s %s %s' % (header, title, header)

        self.suite.totalGroups = self.suite.totalGroups + 1

    ### Methods ###

    def finish(self):
        while self.tests:
            retVal = self.tests[-1].finish()
            self.retVal = self.retVal or retVal
            del self.tests[-1]
        self.suite = None
        return self.retVal
        
    def startTest(self, title):
        test = TestItem(self.suite, title, self)
        self.tests.append(test)
        return test


    def testDone(self):
        if self.tests:
            retVal = self.tests[-1].finish()
            self.retVal = self.retVal or retVal
            del self.tests[-1]
            return self.retVal


class TestSuite:
    totalGroups = 0
    totalItems = 0
    totalCompares = 0

    def __init__(self, stopOnError=1, useColor=0, verbose=2):
        if useColor:
            # Override color if not able to use ANSI control codes
            # FIXME - determine ANSI ability, for right now assume only posix
            useColor = (os.name == 'posix' and sys.stdout.isatty())

        self.useColor = useColor
        self.stopOnError = stopOnError
        self.columns = 80
        self.verbose = verbose
        self.test_data = {}
        self.groups = []
        self.retVal = OK
        self.testTime = 0.0
        self._compareCtr = 0

        self._report = {SKIPPED : [],
                        FAILED : [],
                        }

    def __del__(self):
        while len(self.groups):
            self.groups[-1].finish()
            del self.groups[-1]

    ### Testing Methods ###

    def startGroup(self, title):
        group = TestGroup(self, title)
        self.groups.append(group)
        return group

    def groupDone(self):
        if self.groups:
            retVal = self.groups[-1].finish()
            self.retVal = self.retVal or retVal
            del self.groups[-1]
        return self.retVal

    def startTest(self, title):
        if not self.groups:
            self.startGroup(self)
            print 'Added (null) group'
        test = self.groups[-1].startTest(title)
        self._compareCtr = 0
        return test

    def testDone(self):
        if self.groups:
            self.groups[-1].testDone()
        self._compareCtr = 0

    def testResults(self, expected, actual, done=1, msg=None, func=None, diff=0):

        self.totalCompares = self.totalCompares + 1

        start = time.time()
        try:
            if type(expected) == type(actual) == types.FloatType:
                if number.finite(expected):
                    expected = float(str(expected))
                elif number.isnan(expected):
                    expected = 'NaN'
                elif number.isinf(expected) > 0:
                    expected = 'Inf'
                else:
                    expected = '-Inf'

                if number.finite(actual):
                    actual = float(str(actual))
                elif number.isnan(actual):
                    actual = 'NaN'
                elif number.isinf(actual) > 0:
                    actual = 'Inf'
                else:
                    actual = '-Inf'

            func = func or cmp
            self._compareCtr = self._compareCtr + 1
            if msg is None:
                msg = "Test %d in of %s" % (self._compareCtr,self.groups[-1].tests[-1].title)

            if func(expected, actual):
                msg and self.message(msg)
                if diff:
                    self.diff(expected,actual)
                self.error("Original: %s\nCompared: %s" % (
                    repr(expected),
                    repr(actual),
                    ))
                return 0
        finally:
            end = time.time()
            self.groups[-1].tests[-1].compareTime = self.groups[-1].tests[-1].compareTime + end - start

        if done:
            self.testDone()
        return 1

    def testException(self,func,args,eClass,eCodes=None):
        eCodes = eCodes or {}
        try:
            apply(func,args)
        except eClass,e:
            for attrName,value in eCodes.items():
                if hasattr(e,attrName) and getattr(e,attrName) == value:
                    continue
                self.error("Wrong Exception Attr %s == %s"%(attrName,str(value)),1)
        except:
            self.error("Wrong Exception Raised",1)
        else:
            self.error("No Exception Raised")

        
    def compare(self, expected, actual, msg=None, func=None, diff=0):
        """
        func is a function that returns the same as builtin function cmp
        """
        return self.testResults(expected, actual, 0, msg, func, diff)

    def compareIn(self,expected,actual,msg=None):
        """Test that actual is in expected"""
        func = lambda expected, actual: not (actual in expected)
        return self.testResults(expected, actual, 0, msg, func)
        
    
    def message(self, msg):
        if self.groups:
            if self.groups[-1].tests:
                self.groups[-1].tests[-1].message(msg)

    def warning(self, msg):
        if self.groups:
            nested = map(lambda g: g.title, self.groups)
            if self.groups[-1].tests:
                self.groups[-1].tests[-1].warning(msg)
                nested.append(self.groups[-1].tests[-1].title)
            self._report[SKIPPED].append((nested, msg))

    def error(self, msg, saveTrace=0):
        if self.groups:
            nested = map(lambda g: g.title, self.groups)
            if self.groups[-1].tests:
                self.groups[-1].tests[-1].error(msg, saveTrace)
                nested.append(self.groups[-1].tests[-1].title)
            self._report[FAILED].append((nested, msg))

    def diff(self,expected,actual):
        import tempfile

        if sys.platform[:3] != 'win':
            if self.verbose >= VB_SHOW_DEBUGS:
                sys.stdout.write("Diff of expected and actual\n")
                fn1 = tempfile.mktemp()
                fn2 = tempfile.mktemp()
                f1 = open(fn1,'w')
                f2 = open(fn2,'w')
                f1.write(str(expected))
                f2.write(str(actual))
                f1.close()
                f2.close()
                
                os.system("diff -c %s %s" % (fn1,fn2))
                
                os.unlink(fn1)
                os.unlink(fn2)
        

    def report(self):
        print
        print '='*72
        print
        if self._report[SKIPPED]:
            print "Warnings:"
            for titles, warning in self._report[SKIPPED]:
                prefix = string.join(titles, ': ')
                print '%s: %s' % (prefix, warning)
            print
            print '-'*50

        if self._report[FAILED]:
            print "Errors:"
            for titles, error in self._report[FAILED]:
                prefix = string.join(titles, ': ')
                print '%s: %s' % (prefix, error)
            print
            print '-'*50

        print "Test Groups Run:  %d" % self.totalGroups
        print "Test Items Runs:  %d" % self.totalItems
        print "Results Compared: %d" % self.totalCompares
        print 'Total time for tests (in seconds): %0.2f' % self.testTime
        print

