# -*- coding: utf-8 -*-

# Copyright (c) 2002 - 2006 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the UI to the pyunit package.
"""

import unittest
import string
import sys
import traceback
import time
import re
import os

from qt import *

from KdeQt import KQFileDialog, KQMessageBox

from UnittestForm import UnittestForm

import DebugClients.Python.PyCoverage as PyCoverage

import UI.PixmapCache

class UnittestDialog(UnittestForm):
    """
    Class implementing the UI to the pyunit package.
    
    @signal unittestFile(string,int,int) emitted to show the source of a unittest file
    """
    def __init__(self,prog = None,dbs = None,ui = None,parent = None,name = None,fl = 0):
        """
        Constructor
        
        @param prog filename of the program to open
        @param dbs reference to the debug server object. It is an indication
                whether we were called from within the eric3 IDE
        @param ui reference to the UI object
        @param parent parent widget of this dialog (QWidget)
        @param name name of this dialog (string or QString)
        @param modal flag indicating a modal dialog
        @param fl window flags
        """
        UnittestForm.__init__(self,parent,name,fl)
        
        self.dbs = dbs
        
        self.setIcon(UI.PixmapCache.getPixmap("eric.png"))
        self.setCaption(self.trUtf8("Unittest"))
        if dbs:
            self.closeButton.setText(self.trUtf8("&Exit"))
            self.ui = ui
        else:
            from UI.IconProvider import IconProvider
            self.iconProvider = IconProvider()
            KQFileDialog.setIconProvider(self.iconProvider)
            self.optionsButtonGroup.hide()
        
        self.fileHistory = QStringList()
        self.running = 0
        self.savedModulelist = None
        self.savedSysPath = sys.path
        if prog:
            self.insertProg(prog)
            
        self.rx1 = QRegExp(self.trUtf8("^Failure: "))
        self.rx2 = QRegExp(self.trUtf8("^Error: "))
        
        # now connect the debug server signals if called from the eric3 IDE
        if self.dbs:
            self.connect(self.dbs, PYSIGNAL('utPrepared'),
                self.handleUTPrepared)
            self.connect(self.dbs, PYSIGNAL('utFinished'),
                self.handleStopped)
            self.connect(self.dbs, PYSIGNAL('utStartTest'),
                self.handleTestStarted)
            self.connect(self.dbs, PYSIGNAL('utStopTest'),
                self.handleTestFinished)
            self.connect(self.dbs, PYSIGNAL('utTestFailed'),
                self.handleTestFailed)
            self.connect(self.dbs, PYSIGNAL('utTestErrored'),
                self.handleTestErrored)

    def colorizeProgressbar(self, color):
        """
        Private methode to set the color of the progressbar.
        
        @param color colour for the progressbar
        """
        pal = self.progressProgressBar.palette()
        pal.setColor(QColorGroup.Highlight, color)
        self.progressProgressBar.setPalette(pal)
        
    def insertProg(self, prog):
        """
        Public slot to insert the filename prog into the testsuiteComboBox object.
        
        @param prog filename to be inserted /string or QString)
        """
        # prepend the selected file to the testsuite combobox
        if prog is None:
            prog = QString.null
        self.fileHistory.remove(prog)
        self.fileHistory.prepend(prog)
        self.testsuiteComboBox.clear()
        self.testsuiteComboBox.insertStringList(self.fileHistory)
        
    def handleFileDialog(self):
        """
        Private slot to open a file dialog.
        """
        prog = KQFileDialog.getOpenFileName(QString.null, 
                    self.trUtf8("Python Files (*.py)"), self)
        
        if prog.isNull():
            return
        
        self.insertProg(QDir.convertSeparators(prog))
        
    def handleStartTest(self):
        """
        Public slot to start the test.
        """
        if self.running:
            return
        
        prog = unicode(self.testsuiteComboBox.currentText())
        if not prog:
            KQMessageBox.critical(self, 
                    self.trUtf8("Unittest"), 
                    self.trUtf8("You must enter a test suite file."))
            return
        
        # prepend the selected file to the testsuite combobox
        self.insertProg(prog)
        self.sbLabel.setText(self.trUtf8("Preparing Testsuite"))
        qApp.processEvents()
        
        # build the module name from the filename without extension
        self.testName, dummy = os.path.splitext(os.path.basename(prog))
        
        if self.dbs and not self.localCheckBox.isChecked():
            # we are cooperating with the eric3 IDE
            project = self.ui.getProject()
            if project.isOpen() and project.isProjectSource(prog):
                mainScript = project.getMainScript(1)
            else:
                mainScript = os.path.abspath(prog)
            self.dbs.remoteUTPrepare(prog, self.testName, 
                self.coverageCheckBox.isChecked(), mainScript,
                self.coverageEraseCheckBox.isChecked())
        else:
            # we are running as an application
            sys.path = [os.path.dirname(os.path.abspath(prog))] + self.savedSysPath
            
            # clean up list of imported modules to force a reimport upon running the test
            if self.savedModulelist:
                for modname in sys.modules.keys():
                    if not self.savedModulelist.has_key(modname):
                        # delete it
                        del(sys.modules[modname])
            self.savedModulelist = sys.modules.copy()
            
            # now try to generate the testsuite
            try:
                module = __import__(self.testName)
                try:
                    test = unittest.defaultTestLoader.loadTestsFromName("suite", module)
                except AttributeError:
                    test = unittest.defaultTestLoader.loadTestsFromModule(module)
            except:
                exc_type, exc_value, exc_tb = sys.exc_info()
                KQMessageBox.critical(self, 
                        self.trUtf8("Unittest"),
                        self.trUtf8("<p>Unable to run test <b>%1</b>.<br>%2<br>%3</p>")
                            .arg(self.testName)
                            .arg(str(exc_type))
                            .arg(str(exc_value)))
                return
                
            # now set up the coverage stuff
            if self.dbs and self.coverageCheckBox.isChecked():
                # we are cooperating with the eric3 IDE
                project = self.ui.getProject()
                if project.isOpen() and project.isProjectSource(prog):
                    mainScript = project.getMainScript(1)
                else:
                    mainScript = os.path.abspath(prog)
                cover = PyCoverage.coverage(mainScript)
                if self.coverageEraseCheckBox.isChecked():
                    cover.erase()
            else:
                cover = None
            
            self.testResult = QtTestResult(self)
            self.totalTests = test.countTestCases()
            self.handleRunning()
            if cover:
                cover.start()
            test.run(self.testResult)
            if cover:
                cover.stop()
                cover.save()
            self.handleStopped()
            sys.path = self.savedSysPath
        
    def handleUTPrepared(self, nrTests, exc_type, exc_value):
        """
        Private slot to handle the utPrepared signal.
        
        If the unittest suite was loaded successfully, we ask the
        client to run the test suite.
        
        @param nrTests number of tests contained in the test suite (integer)
        @param exc_type type of exception occured during preparation (string)
        @param exc_value value of exception occured during preparation (string)
        """
        if nrTests == 0:
            KQMessageBox.critical(self, 
                    self.trUtf8("Unittest"),
                    self.trUtf8("<p>Unable to run test <b>%1</b>.<br>%2<br>%3</p>")
                        .arg(self.testName)
                        .arg(exc_type)
                        .arg(exc_value))
            return
            
        self.totalTests = nrTests
        self.handleRunning()
        self.dbs.remoteUTRun()
        
    def handleStopTest(self):
        """
        Private slot to stop the test.
        """
        if self.dbs and not self.localCheckBox.isChecked():
            self.dbs.remoteUTStop()
        elif self.testResult:
            self.testResult.stop()
            
    def handleErrorHighlighted(self, text):
        """
        Private slot to handle the highlighted(const QString&) signal.
        """
        text.remove(self.rx1).remove(self.rx2)
        itm = self.testsListBox.findItem(text)
        self.testsListBox.setCurrentItem(itm)
        self.testsListBox.ensureCurrentVisible()
        
    def handleRunning(self):
        """
        Private method to set the GUI in running mode.
        """
        self.running = 1
        
        # reset counters and error infos
        self.runCount = 0
        self.failCount = 0
        self.errorCount = 0
        self.remainingCount = self.totalTests
        self.errorInfo = []

        # reset the GUI
        self.progressCounterRunCount.setText(str(self.runCount))
        self.progressCounterFailureCount.setText(str(self.failCount))
        self.progressCounterErrorCount.setText(str(self.errorCount))
        self.progressCounterRemCount.setText(str(self.remainingCount))
        self.errorsListBox.clear()
        self.errorsListBox.setCurrentItem(None)
        self.testsListBox.clear()
        self.testsListBox.setCurrentItem(None)
        self.progressProgressBar.setTotalSteps(self.totalTests)
        self.colorizeProgressbar(QColor("green"))
        self.progressProgressBar.reset()
        self.stopButton.setEnabled(1)
        self.startButton.setEnabled(0)
        self.sbLabel.setText(self.trUtf8("Running"))
        qApp.processEvents()
        
        self.startTime = time.time()
        
    def handleStopped(self):
        """
        Private method to set the GUI in stopped mode.
        """
        self.stopTime = time.time()
        self.timeTaken = float(self.stopTime - self.startTime)
        self.running = 0
        
        self.startButton.setEnabled(1)
        self.stopButton.setEnabled(0)
        if self.runCount == 1:
            self.sbLabel.setText(self.trUtf8("Ran %1 test in %2s")
                .arg(self.runCount)
                .arg("%.3f" % self.timeTaken))
        else:
            self.sbLabel.setText(self.trUtf8("Ran %1 tests in %2s")
                .arg(self.runCount)
                .arg("%.3f" % self.timeTaken))

    def handleTestFailed(self, test, exc):
        """
        Public method called if a test fails.
        
        @param test name of the failed test (string)
        @param exc string representation of the exception (list of strings)
        """
        self.failCount = self.failCount + 1
        self.progressCounterFailureCount.setText(str(self.failCount))
        self.errorsListBox.insertItem(self.trUtf8("Failure: %1").arg(test), 0)
        self.errorInfo.insert(0, (test, exc))
        
    def handleTestErrored(self, test, exc):
        """
        Public method called if a test errors.
        
        @param test name of the failed test (string)
        @param exc string representation of the exception (list of strings)
        """
        self.errorCount = self.errorCount + 1
        self.progressCounterErrorCount.setText(str(self.errorCount))
        self.errorsListBox.insertItem(self.trUtf8("Error: %1").arg(test), 0)
        self.errorInfo.insert(0, (test, exc))
        
    def handleTestStarted(self, test, doc):
        """
        Public method called if a test is about to be run.
        
        @param test name of the started test (string)
        @param doc documentation of the started test (string)
        """
        if doc:
            self.testsListBox.insertItem("    %s" % doc, 0)
        self.testsListBox.insertItem(unicode(test), 0)
        if self.dbs is None or self.localCheckBox.isChecked():
            qApp.processEvents()
        
    def handleTestFinished(self):
        """
        Public method called if a test has finished.
        
        <b>Note</b>: It is also called if it has already failed or errored.
        """
        # update the counters
        self.remainingCount = self.remainingCount - 1
        self.runCount = self.runCount + 1
        self.progressCounterRunCount.setText(str(self.runCount))
        self.progressCounterRemCount.setText(str(self.remainingCount))
        
        # update the progressbar
        if self.errorCount:
            self.colorizeProgressbar(QColor("red"))
        elif self.failCount:
            self.colorizeProgressbar(QColor("orange"))
        self.progressProgressBar.setProgress(self.runCount)
        
    def handleListboxDoubleClick(self, lbitem):
        """
        Private slot called by doubleclicking an errorlist entry.
        
        It will popup a dialog showing the stacktrace.
        If called from eric, an additional button is displayed
        to show the python source in an eric source viewer (in
        erics main window.
        
        @param lbitem the listbox item that was double clicked
        """
        self.errListIndex = self.errorsListBox.index(lbitem)
        text = lbitem.text()

        # get the error info
        test, tracebackLines = self.errorInfo[self.errListIndex]
        tracebackText = string.join(tracebackLines, "")

        # now build the dialog
        self.dlg = QDialog()
        self.dlg.setCaption(text)
        self.dlg.resize(600,250)
        self.dlg.layout = QVBoxLayout(self.dlg,6,6)

        self.dlg.testLabel = QLabel(test, self.dlg)
        self.dlg.layout.addWidget(self.dlg.testLabel)

        self.dlg.traceback = QTextEdit(tracebackText, None, self.dlg)
        self.dlg.traceback.setTextFormat(Qt.PlainText)
        self.dlg.traceback.setReadOnly(1)
        self.dlg.layout.addWidget(self.dlg.traceback)

        buttonLayout = QHBoxLayout(None,0,6)
        spacer = QSpacerItem(0,0,QSizePolicy.Expanding,QSizePolicy.Minimum)
        buttonLayout.addItem(spacer)
        self.dlg.closeButton = QPushButton(self.trUtf8("&Close"), self.dlg)
        self.dlg.closeButton.setDefault(1)
        self.dlg.connect(self.dlg.closeButton, SIGNAL("clicked()"),
                        self.dlg, SLOT("accept()"))
        buttonLayout.addWidget(self.dlg.closeButton)
        # one more button if called from eric
        if self.dbs:
            self.dlg.showButton = QPushButton(self.trUtf8("&Show Source"), 
                            self.dlg)
            self.dlg.connect(self.dlg.showButton, SIGNAL("clicked()"),
                            self.handleShowSource)
            buttonLayout.addWidget(self.dlg.showButton)
        spacer = QSpacerItem(0,0,QSizePolicy.Expanding,QSizePolicy.Minimum)
        buttonLayout.addItem(spacer)
        self.dlg.layout.addLayout(buttonLayout)

        # and now fire it up
        self.dlg.show()
        self.dlg.exec_loop()
        
    def handleShowSource(self):
        """
        Private slot to show the source of a traceback in an eric3 editor.
        """
        if not self.dbs:
            return
            
        # get the error info
        test, tracebackLines = self.errorInfo[self.errListIndex]
        fmatch = re.search(r'File "(.*?)", line (\d*?),.*', tracebackLines[1])
        if fmatch:
            fn, ln = fmatch.group(1, 2)
            self.emit(PYSIGNAL('unittestFile'),(fn,int(ln),1))

class QtTestResult(unittest.TestResult):
    """
    A TestResult derivative to work with a graphical GUI.
    
    For more details see pyunit.py of the standard python distribution.
    """
    def __init__(self, parent):
        """
        Constructor
        
        @param parent The parent widget.
        """
        unittest.TestResult.__init__(self)
        self.parent = parent
        
    def addFailure(self, test, err):
        """
        Method called if a test failed.
        
        @param test Reference to the test object
        @param err The error traceback
        """
        unittest.TestResult.addFailure(self, test, err)
        tracebackLines = apply(traceback.format_exception, err + (10,))
        self.parent.handleTestFailed(unicode(test), tracebackLines)
        
    def addError(self, test, err):
        """
        Method called if a test errored.
        
        @param test Reference to the test object
        @param err The error traceback
        """
        unittest.TestResult.addError(self, test, err)
        tracebackLines = apply(traceback.format_exception, err + (10,))
        self.parent.handleTestErrored(unicode(test), tracebackLines)
        
    def startTest(self, test):
        """
        Method called at the start of a test.
        
        @param test Reference to the test object
        """
        unittest.TestResult.startTest(self, test)
        self.parent.handleTestStarted(unicode(test), test.shortDescription())

    def stopTest(self, test):
        """
        Method called at the end of a test.
        
        @param test Reference to the test object
        """
        unittest.TestResult.stopTest(self, test)
        self.parent.handleTestFinished()
