
# Author     : Tim Couper - timc@tizmoi.net
# Date       : 1 August 2004
# Copyright  : Copyright TAC Software Ltd, under Python-like licence.
#              Provided as-is, with no warranty.
# Notes      : Requires wxPython,watsup
#              Based heavily on unittestgui.py by Chris Liechti

import unittest, os, imp, time, traceback,sys

## import all of the wxPython GUI package
from wxPython.wx import *
from wxPython.grid import *

from watsup.performance import doChecking,onOff,isChecking
from watsup.tools.buildSuite import buildSuite
from time import ctime

BASE_TITLE="Watsup Framework"
INI_FILE='./runTests.ini'

from ConfigParser import ConfigParser,NoOptionError, NoSectionError
  
#specify editor, make it return immediately, otherwise the GUI will wait
#until its closed. on win use "start cmd", on un*x "cmd&"
EDITOR = r'start c:\tools\wscite\scite "%(filename)s" -goto:%(lineno)s'
def starteditor(filename, lineno=1):
    if os.path.exists(filename):
        os.system(EDITOR % {'filename':filename, 'lineno':lineno})
    else:
        wxMessageBox("Cannot locate sourcefile:\n%s" % filename, "Can't start editor...", wxOK)
        
#----------------------------------------------------------------

class GUITestResult(unittest.TestResult):
    """A test result class that can print formatted text results to a stream.
    """
    separator1 = '=' * 70
    separator2 = '-' * 70

    def __init__(self, listview, progress, descriptions, verbosity):
        unittest.TestResult.__init__(self)
        self.listview = listview
        self.showAll = verbosity > 1
        self.descriptions = descriptions
        self.progress = progress
        self.testdescr = None
        self.stream = sys.stdout

    def getDescription(self, test):
        if self.descriptions:
            return test.shortDescription() or str(test)
        else:
            return str(test)

    def startTest(self, test):
        unittest.TestResult.startTest(self, test)
        self.testdescr = self.getDescription(test)
        if self.showAll:
            self.stream.write(self.getDescription(test))
            self.stream.write(" ... ")

    def _correctFilename(self, filename):
        if filename[-4:] in ('.pyc', '.pyo'):
            return filename[:-1]
        return filename
            
    def addSuccess(self, test):
        unittest.TestResult.addSuccess(self, test)
        if self.showAll:
            self.stream.write("ok\n")
        self.listview.Append( (
            'Ok',
            self.testdescr,
            '',
            None,
            None
            ) )
        self.progress.tick()
        
    def addError(self, test, err):
        unittest.TestResult.addError(self, test, err)
        if self.showAll:
            self.stream.write("ERROR\n")
        try:
            filename = self._correctFilename(err[2].tb_frame.f_globals['__file__'])
        except KeyError:
            filename = None
        lineno = err[2].tb_lineno
        self.listview.Append( (
            'Error',
            self.testdescr,
            traceback.format_exception(*err)[-1].rstrip(),
            self._exc_info_to_string(err),
            (filename, lineno)
            ) )
        self.progress.tick()

    def addFailure(self, test, err):
        unittest.TestResult.addFailure(self, test, err)
        if self.showAll:
            self.stream.write("FAIL\n")
        try:
            filename = self._correctFilename(err[2].tb_frame.f_globals['__file__'])
        except KeyError:
            filename = None
        lineno = err[2].tb_lineno
        self.listview.Append( (
            'Fail',
            self.testdescr,
            traceback.format_exception(*err)[-1].rstrip(),
            self._exc_info_to_string(err),
            (filename, lineno)
            ) )
        self.progress.tick()

    def printErrors(self):
        if self.showAll:
            self.stream.write("\n")
        self.printErrorList('ERROR', self.errors)
        self.printErrorList('FAIL', self.failures)

    def printErrorList(self, flavour, errors):
        for test, err in errors:
            self.stream.write(self.separator1)
            self.stream.write("\n%s: %s\n" % (flavour,self.getDescription(test)))
            self.stream.write(self.separator2)
            self.stream.write("\n%s\n" % err)

class GUITestRunner:
    """A test runner class that displays results in textual form.

    It prints out the names of tests as they are run, errors as they
    occur, and a summary of the results at the end of the test run.
    """
    def __init__(self, listview, progress, stream=sys.stderr, descriptions=1, verbosity=2):
        self.listview = listview
        self.progress = progress
        self.stream = unittest._WritelnDecorator(stream)
        self.descriptions = descriptions
        self.verbosity = verbosity

    def _makeResult(self):
        return GUITestResult(self.listview, self.progress, self.descriptions, self.verbosity)

    def run(self, test):
        "Run the given test case or test suite."
        result = self._makeResult()
        startTime = time.time()
        test(result)
        stopTime = time.time()
        timeTaken = float(stopTime - startTime)
        result.printErrors()
        self.stream.writeln(result.separator2)
        run = result.testsRun
        self.stream.writeln("Ran %d test%s in %.3fs" %
                            (run, run == 1 and "" or "s", timeTaken))
        self.stream.writeln()
        if not result.wasSuccessful():
            self.stream.write("FAILED (")
            failed, errored = map(len, (result.failures, result.errors))
            if failed:
                self.stream.write("failures=%d" % failed)
            if errored:
                if failed: self.stream.write(", ")
                self.stream.write("errors=%d" % errored)
            self.stream.writeln(")")
        else:
            self.stream.writeln("OK")
                
        return result

#----------------------------------------------------------------

def lastline(t):
    t = t.rstrip()
    n = t.rfind('\n')
    if n >= 0:
        return t[n+1:]
    return t

#----------------------------------------------------------------

class ResultView(wxListCtrl):
    PM_DETAIL = wxNewId()
    PM_LOCATE = wxNewId()
    
    def __init__(self, parent):
        wxListCtrl.__init__(self, parent, -1,
                            style=wxLC_REPORT|wxSUNKEN_BORDER|wxLC_VIRTUAL|wxLC_VRULES,#|wxLC_HRULES,
                            size=(500,200))

        self.InsertColumn(0, '-Ok-')
        self.InsertColumn(1, 'Description')
        self.InsertColumn(2, 'Details')

        self.SetColumnWidth(0, 40)
        w = self.GetSize()[0] - self.GetColumnWidth(0)
        self.SetColumnWidth(1, w/2)
        self.SetColumnWidth(2, w/2)

        EVT_RIGHT_DOWN(self, self.OnRightClick)
        EVT_LIST_ITEM_SELECTED(self, self.GetId(), self.OnItemSelected)
        EVT_LIST_ITEM_ACTIVATED(self, self.GetId(), self.OnItemActivated)
        
        self.red = wxListItemAttr()
        self.red.SetBackgroundColour(wxRED)
        self.green = wxListItemAttr()
        self.green.SetBackgroundColour(wxColour(200,255,200))   #light green
        self.orange = wxListItemAttr()
        self.orange.SetBackgroundColour(wxColour(255,200,200))  #light red
        self.result = []
        self.currentItem = None

        EVT_SIZE(self, self.OnSize)

        #node menu
        self.nodemenu = wxMenu()
        self.nodemenu.Append(self.PM_DETAIL, "Show Details")
        #self.nodemenu.AppendSeparator()
        #self.nodemenu.Append(self.PM_LOCATE, "Locate Source")
       
        EVT_MENU(self, self.PM_DETAIL, self.OnDetail)
        #EVT_MENU(self, self.PM_LOCATE, self.OnLocate)

    def OnDetail(self, event=None):
        if self.result[self.currentItem][3]:
            wxMessageBox(self.result[self.currentItem][3], "Result details", wxOK)

    def OnLocate(self, event=None):
        filename, lineno = self.result[self.currentItem][4]
        print "locate",filename, lineno
        starteditor(filename, lineno)

    def OnSize(self, event):
        w = self.GetSize()[0] - self.GetColumnWidth(0) - 30
        if w < 180: w = 180
        self.SetColumnWidth(1, w/2)
        self.SetColumnWidth(2, w/2)

    def Append(self, item):
        self.result.append(item)
        self.SetItemCount(len(self.result))
        #wxYield()
        #self.Refresh()

    def DeleteAllItems(self):
        self.result = []
        wxListCtrl.DeleteAllItems(self)

    def OnItemSelected(self, event):
        self.currentItem = event.m_itemIndex

    def OnItemActivated(self, event):
        self.currentItem = event.m_itemIndex
        self.OnDetail()

    def OnRightClick(self, event):
        pt = event.GetPosition()
        item, flags = self.HitTest(pt)
        if not flags & wxLIST_HITTEST_NOWHERE:
            if self.currentItem is not None:
                self.SetItemState(self.currentItem, 0, wxLIST_STATE_SELECTED  )
            self.currentItem = item
            self.SetItemState(item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED  )
            if self.result[self.currentItem][3]:
                self.PopupMenu(self.nodemenu, pt) #display popup menu

    def OnCopyAsText(self, all=0):
        res = []
        item = -1;
        while 1:
            if all:
                item = self.GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_DONTCARE);
            else:
                item = self.GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
            text = '\t'.join(map(str, self.result[item]))
            res.append(text)
            if item == -1:
                break
        clip = wxClipboard()
        clip.Open()
        clip.SetData( wxTextDataObject('\n'.join(res)) )
        clip.Close()
        
    #---------------------------------------------------
    # These methods are callbacks for implementing the
    # "virtualness" of the list...
    def OnGetItemText(self, row, col):
        #print row, col
        try:
            return str(self.result[row][col])
        except IndexError:
            return ''

    def OnGetItemImage(self, item):
        return 0

    def OnGetItemAttr(self, item):
        if self.result[item][0] == 'Error':
            return self.red
        elif self.result[item][0] == 'Fail':
            return self.orange
        elif self.result[item][0] == 'Ok':
            return self.green
        return None

class TreeView(wxTreeCtrl):
    PM_RUNSEL = wxNewId()
    PM_LOCATE = wxNewId()
    
    def __init__(self, *args, **kwargs):
        wxTreeCtrl.__init__(self, *args, **kwargs)
        EVT_LEFT_DCLICK(self, self.OnLeftDClick)
        EVT_RIGHT_DOWN(self, self.OnRightClick)
        EVT_RIGHT_UP(self, self.OnRightUp)
        EVT_TREE_BEGIN_LABEL_EDIT(self, self.GetId(), self.OnBeginEdit)
        EVT_TREE_END_LABEL_EDIT (self, self.GetId(), self.OnEndEdit)

        #node menu
##        self.nodemenu = wxMenu()
##        self.nodemenu.Append(self.PM_RUNSEL, "Run selected")
##        self.nodemenu.AppendSeparator()
##        self.nodemenu.Append(self.PM_LOCATE, "Locate Source")
##       
##        EVT_MENU(self, self.PM_RUNSEL, self.OnRunSel)
##        EVT_MENU(self, self.PM_LOCATE, self.OnLocate)

    #don't allow editing of node names
    def OnBeginEdit(self, event):  event.Veto()
    def OnEndEdit(self, event):    event.Veto()

    def OnLeftDClick(self, event):
##        pt = event.GetPosition();
##        item, flags = self.HitTest(pt)
##        #print("OnLeftDClick: %s" % self.GetItemText(item))
##        parent = self.GetItemParent(item)
##        #self.SortChildren(parent)
        event.Skip()

    def OnRightClick(self, event):
        pt = event.GetPosition()
        item, flags = self.HitTest(pt)
        if not flags & wxTREE_HITTEST_NOWHERE:
            self.SelectItem(item)
            #self.PopupMenu(self.nodemenu, pt) #display node menu

    def OnRightUp(self, event):
        pt = event.GetPosition();
        item, flags = self.HitTest(pt)
        #self.tree.EditLabel(item)

    def OnCompareItems(self, item1, item2):
        t1 = self.GetItemText(item1)
        t2 = self.GetItemText(item2)
        #print('compare: ' + t1 + ' <> ' + t2)
        return cmp(t1,t2)

    def OnRunSel(self, event):
        pass
    def OnLocate(self, event):
        pass

#----------------------------------------------------------------

class ProgressBar(wxGauge):
    def __init__(self, *args, **kwargs):
        wxGauge.__init__(self, *args, **kwargs)
        self.SetBezelFace(5)
        self.SetShadowWidth(5)
        self.increment = 1

    def tick(self):
        self.SetValue(self.GetValue() + self.increment)

#----------------------------------------------------------------

class Frame(wxFrame):
    ID_BUTTON   = wxNewId()
    #M_NEW       = wxNewId()
    M_OPENFILES = wxNewId()
    M_OPENDIR   = wxNewId()
    M_OPENTREE  = wxNewId()
    M_EXIT      = wxNewId()
    M_COPYALL   = wxNewId()
    M_COPYSEL   = wxNewId()
    M_RUN       = wxNewId()
    M_CHECKING  = wxNewId()
    

    def __init__(self, parent, id, title = BASE_TITLE):
        
        # First, call the base class' __init__ method to create the frame
        wxFrame.__init__(self, parent, id, title, wxPoint(100, 100), wxSize(100, 100))
        #variables
        self.suite = None
                
        self.loadConfig()
               
        #menu
        menuBar = wxMenuBar()
        menu = wxMenu()
        #menu.Append(self.M_NEW, "&New")
        menu.Append(self.M_OPENFILES, "&Load Test File(s)")
        menu.Append(self.M_OPENDIR, "&Load Test Directory")
        menu.Append(self.M_OPENTREE, "&Load Test Directory Tree")
        menu.AppendSeparator()
        menu.Append(self.M_EXIT, "E&xit")
        #EVT_MENU(self, self.M_NEW,  self.OnNew)
        EVT_MENU(self, self.M_OPENFILES, self.OnMenuOpenFiles)
        EVT_MENU(self, self.M_OPENDIR, self.OnMenuOpenDir)
        EVT_MENU(self, self.M_OPENTREE, self.OnMenuOpenTree)
        EVT_MENU(self, self.M_EXIT, self.OnCloseWindow)
        menuBar.Append(menu, "&File");

        menu = wxMenu()
        menu.Append(self.M_COPYALL, "&Copy all results")
        menu.Append(self.M_COPYSEL, "&Copy selected results")
        EVT_MENU(self, self.M_COPYALL, self.OnCopyAll)
        EVT_MENU(self, self.M_COPYSEL, self.OnCopySelection)
        menuBar.Append(menu, "&Edit");

        menu = wxMenu()
        menu.Append(self.M_RUN, "&Run all")
        menu.AppendSeparator()
        menu.AppendCheckItem(self.M_CHECKING,"&Checking")
        menu.Check(self.M_CHECKING,isChecking())
        EVT_MENU(self, self.M_RUN, self.OnRun)
        EVT_MENU(self, self.M_CHECKING, self.OnChecking)        
        self.testing_menu=menu
        menuBar.Append(menu, "&Testing")
            
        self.SetMenuBar(menuBar)

        #GUI
        panel = wxPanel(self, 0)

        button = wxButton(panel, self.ID_BUTTON, "Run Tests" )
        button.SetDefault()
        EVT_BUTTON(panel, self.ID_BUTTON, self.OnRun )
 
        self.tree = TreeView(panel, -1, size=(300,250), style=wxTR_HAS_BUTTONS | wxTR_EDIT_LABELS)# | wxTR_MULTIPLE
        
        self.progress =  ProgressBar(panel, -1, 100, style=wxGA_HORIZONTAL|wxGA_SMOOTH)
        
        self.list = ResultView(panel)
        
        sizer = wxBoxSizer(wxVERTICAL)
        sizer.Add(self.tree, 1, wxEXPAND)
        sizer.Add(button, 0, wxEXPAND)
        sizer.Add(self.progress, 0, wxEXPAND)
        sizer.Add(self.list, 1, wxEXPAND)

        panel.SetAutoLayout(1)
        panel.SetSizer(sizer)
        sizer.Fit(panel)

        basesizer = wxBoxSizer(wxVERTICAL)
        basesizer.Add(panel, 1, wxEXPAND)
        self.SetAutoLayout(1)
        self.SetSizer(basesizer)
        self.Fit()

        #create a statusbar
        sb = self.CreateStatusBar(1)
        sb.SetStatusWidths([-1])
        self.SetStatusText('Please load file(s), a directory or tree',0)
        #update controls
        self.OnNew()
    
    def __del__(self):
        # persist any values in .ini file 
                
        cfg=ConfigParser()        
        cfg.add_section('main')
        cfg.set('main','current_directory',self.getCurrentDirectory())
        cfg.set('main','checking',isChecking())
        cfg.write(open(INI_FILE,'w'))
        
    def loadConfig(self): 
        # load settings from the config file       
        
        cfg=ConfigParser()
        cfg.read(INI_FILE)
    
        self._currentDirectory='.'
        try:
            self.setCurrentDirectory(cfg.get('main','current_directory'))
        except NoSectionError: # none of the other options will be available
            return
        except NoOptionError:
            pass
                   
        try:
            doChecking(cfg.getboolean('main','checking'))
        except (NoOptionError, NoSectionError):
            doChecking(True)
           
               
    def UpdateTree(self, root=None, testlist=None):
        if root is None:
            root = self.root
        if testlist is None and self.suite:
            testlist = self.suite._tests #grmpf accessing a _ member, oh well

        if root and testlist:  
            for testcase in testlist:               
                if isinstance(testcase, unittest.TestSuite):
                
                    testname=testcase._tests[0]
                    #str(testname): atestcasename (module.class)
                    # we want module.class here, with no brackets
                    # this should do it
                    label= str(testname).split('(')[1]
                    label=label[:-1]
                        
                    child = self.tree.AppendItem(root, "%s (%d)" % (label,len(testcase._tests)))
                    self.tree.SetPyData(child, None)
                    self.UpdateTree(child, testcase._tests)
                    self.tree.SortChildren(child)
                    self.tree.Collapse(child)
                else:
                   
                    label="%s" % testcase 
                    label=label.split('(')[0]                       
                          
                    child = self.tree.AppendItem(root,label)
                    self.tree.SetPyData(child, None)
                ##self.tree.SetItemImage(child, idx2, wxTreeItemIcon_Expanded)
                ##self.tree.SetItemSelectedImage(child, idx3)
           
            self.tree.Expand(root)

    def OnRun(self, event=None):
        """ """
        if self.suite:
            runner = GUITestRunner(self.list, self.progress)
            self.SetStatusText('Running tests...',0)
            ln = self.suite.countTestCases()
            self.progress.SetValue(0)
            self.progress.SetRange(ln)
            self.list.DeleteAllItems()
            
            result = runner.run(self.suite)
            
            self.SetStatusText('Ran %d tests, %d errors, %d failures' % (ln, len(result.errors), len(result.failures)), 0)
            
        else:
            self.SetStatusText('No tests found', 0)
            
        
    def OnMenuOpenFiles(self, event=None):
        """ """
        
        dlg = wxFileDialog(self, "Choose one or more modules", 
                           self.getCurrentDirectory(),"", 
                           'Python files (*.py)|*.py|All files (*.*)|*.*', 
                           wxOPEN|wxMULTIPLE)
                           
        if dlg.ShowModal() == wxID_OK:
##            for path in dlg.GetPaths():
##                log.WriteText('You selected: %s\n' % path)
            self.OnNew()
            filenames = dlg.GetPaths() #dlg.GetPath()
            self.setCurrentDirectory(dlg.GetDirectory())
            self.suite=buildSuite(filenames)
        
            #print self.suite
        dlg.Destroy()
        self.UpdateTree() 
    
    def OnMenuOpenDir(self,event=None):
        self.loadDirectory('Choose the test directory')                       
    
    def OnMenuOpenTree(self,event=None):
        self.loadDirectory('Choose the top test directory',True)
                          
    def loadDirectory(self,msg,tree=False):
        dlg=wxDirDialog(self,msg, self.getCurrentDirectory())                        
        if dlg.ShowModal() == wxID_OK:
            self.OnNew()
            dir = dlg.GetPath() 
            self.setCurrentDirectory(dir)
            self.suite=buildSuite(dir,tree)
             #print self.suite
        dlg.Destroy()
        self.UpdateTree()   
        
    def OnNew(self, event=None):
        self.list.DeleteAllItems()
        self.tree.DeleteAllItems()
        self.root = self.tree.AddRoot("Watsup Tests:")
        self.tree.SetPyData(self.root, None)
        self.progress.SetValue(0)
        self.suite = None
        #self.filename = None

    def OnCopySelection(self,event=None):
        self.list.OnCopyAsText()

    def OnCopyAll(self,event=None):
        self.list.OnCopyAsText(all=1)

    def OnCloseWindow(self, event=None):
        self.Destroy()
    
       
    def OnChecking(self,event=None):
        # toggle self._checking:
        bool=not isChecking()
        self.testing_menu.Check(self.M_CHECKING,bool)
        doChecking(bool)
        self.SetStatusText('Checking %s' % onOff(bool),0)

    def getCurrentDirectory(self):
       if os.path.isdir(self._currentDirectory):
           return self._currentDirectory
       else:
           return '.'
       
    def setCurrentDirectory(self,value):
       if self._currentDirectory<>value:
           if os.path.isdir(value):
               self._currentDirectory=value
               # update the title
               if value=='.':
                   text=''
               else:
                   text='(%s)' % value
                   
               self.SetTitle('%s %s'.strip() % (BASE_TITLE,text))
           else:
               self._currentDirectory='.'    
           
                   
#---------------------------------------------------------------------------
# Every wxWindows application must have a class derived from wxApp
class App(wxApp):
    # wxWindows calls this method to initialize the application
    def OnInit(self):
        # Create an instance of our customized Frame class
        frame = Frame(NULL, -1)
        frame.Show(true)
        # Tell wxWindows that this is our main window
        self.SetTopWindow(frame)
        # Return a success flag
        return true

#---------------------------------------------------------------------------
if __name__ == "__main__":
    app = App(0)    # Create an instance of the application class
    app.MainLoop()     # Tell it to start processing events

