""" :author: Jeremy Ernst, Chad Vernon :description: This module contains classes for running unit tests. Thanks to Chad Vernon (http://www.chadvernon.com/blog/unit-testing-in-maya/) for sharing this. Most of the code below was taken from his blog post. """ import os import unittest import traceback import logging import sys import artv2.utilities.general_utilities as utils import artv2.tools.system.artv2_tester.test_runner from artv2.third_party.Qt import QtGui, QtCore TEST_LOGGER = logging.getLogger(__name__) # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # class TestCaptureStream(object): """ Allows output of tests to be captured and displayed in a qTextEdit """ success_color = QtGui.QColor(92, 184, 92) fail_color = QtGui.QColor(240, 173, 78) error_color = QtGui.QColor(217, 83, 79) skip_color = QtGui.QColor(88, 165, 204) normal_color = QtGui.QColor(200, 200, 200) def __init__(self, text_edit): self.text_edit = text_edit def write(self, text): """ Writes the given text o the widget. :param text: Text to write the widget. """ if text.startswith("ok"): self.text_edit.setTextColor(TestCaptureStream.success_color) elif text.startswith("FAIL"): self.text_edit.setTextColor(TestCaptureStream.fail_color) elif text.startswith("ERROR"): self.text_edit.setTextColor(TestCaptureStream.error_color) elif text.startswith("skipped"): self.text_edit.setTextColor(TestCaptureStream.skip_color) self.text_edit.insertPlainText(text) self.text_edit.setTextColor(TestCaptureStream.normal_color) def flush(self): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # class TestStatus(object): """ Simple class representing different states of tests. """ not_run = 0 success = 1 fail = 2 error = 3 skipped = 4 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # class TestNode(object): """ Class representing a test, test case, or test suite in a qTreeView. """ icon_path = utils.return_settings()[2] success_icon = QtGui.QPixmap(os.path.join(icon_path, "general", "status_passed.png")) fail_icon = QtGui.QPixmap(os.path.join(icon_path, "general", "status_failed.png")) error_icon = QtGui.QPixmap(os.path.join(icon_path, "general", "status_failed.png")) skip_icon = QtGui.QPixmap(os.path.join(icon_path, "general", "status_skipped.png")) def __init__(self, test, parent=None): self.children = [] self._parent = parent if parent is not None: parent.add_child(self) self.test = test self.tool_tip = str(test) self.status = TestStatus.not_run if isinstance(self.test, unittest.TestSuite): for test_ in self.test: if isinstance(test_, unittest.TestCase) or test_.countTestCases(): self.add_child(TestNode(test_, self)) if "ModuleImportFailure" == self.test.__class__.__name__: try: getattr(self.test, self.name())() except ImportError: self.tool_tip = traceback.format_exc() TEST_LOGGER.warning(self.tool_tip) def name(self): if isinstance(self.test, unittest.TestCase): return self.test._testMethodName elif isinstance(self.child(0).test, unittest.TestCase): return self.child(0).test.__class__.__name__ else: return self.child(0).child(0).test.__class__.__module__ def path(self): if self.parent() and self.parent().parent(): return "{0}.{1}".format(self.parent().path(), self.name()) else: return self.name() def get_status(self): if "ModuleImportFailure" in [self.name(), self.test.__class__.__name__]: return TestStatus.error if not self.children: return self.status result = TestStatus.not_run for child in self.children: child_status = child.get_status() if child_status == TestStatus.error: return child_status elif child_status == TestStatus.fail: result = child_status elif child_status == TestStatus.success and result != TestStatus.fail: result = child_status elif child_status == TestStatus.skipped and result != TestStatus.fail: result = child_status return result def get_icon(self): status = self.get_status() return [None, TestNode.success_icon, TestNode.fail_icon, TestNode.error_icon, TestNode.skip_icon][status] def add_child(self, child): if child not in self.children: self.children.append(child) def remove(self): if self._parent: row = self.row() self._parent.children.pop(row) self._parent = None for child in self.children: child.remove() def child(self, row): try: return self.children[row] except IndexError: return None def child_count(self): return len(self.children) def parent(self): return self._parent def row(self): if self._parent is not None: return self._parent.children.index(self) return 0 def data(self, column): return "" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # class TestTreeModel(QtCore.QAbstractItemModel): """ A model used to populate a tree view. """ def __init__(self, root, parent=None): super(TestTreeModel, self).__init__(parent) self._root_node = root self.node_lookup = {} self._create_node_lookup(self._root_node) def _create_node_lookup(self, node): self.node_lookup[str(node.test)] = node for child in node.children: self._create_node_lookup(child) def rowCount(self, parent): if not parent.isValid(): parent_node = self._root_node else: parent_node = parent.internalPointer() return parent_node.child_count() def columnCount(self, parent): return 1 def data(self, index, role): if not index.isValid(): return None node = index.internalPointer() if role == QtCore.Qt.DisplayRole: return node.name() elif role == QtCore.Qt.DecorationRole: return node.get_icon() elif role == QtCore.Qt.ToolTipRole: return node.tool_tip def setData(self, index, value, role=QtCore.Qt.EditRole): node = index.internalPointer() if node is not None: if role == QtCore.Qt.DecorationRole: node.status = value self.dataChanged.emit(index, index) if node.parent() is not self._root_node: self.setData(self.parent(index), value, role) def headerData(self, section, orientation, role): return "Tests" def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable def parent(self, index): node = index.internalPointer() parent_node = node.parent() if parent_node == self._root_node: return QtCore.QModelIndex() return self.createIndex(parent_node.row(), 0, parent_node) def index(self, row, column, parent): if not parent.isValid(): parent_node = self._root_node else: parent_node = parent.internalPointer() child_item = parent_node.child(row) if child_item: return self.createIndex(row, column, child_item) else: return QtCore.QModelIndex() def get_index_of_node(self, node): if node is self._root_node: return QtCore.QModelIndex() return self.index(node.row(), 0, self.get_index_of_node(node.parent())) def run_tests(self, stream, test_suite): runner = unittest.TextTestRunner(stream=stream, verbosity=2, resultclass=artv2.tools.system.artv2_tester.test_runner.CustomTestResult) runner.failfast = False runner.bugger = artv2.tools.system.artv2_tester.test_runner.Settings.buffer_output result = runner.run(test_suite) self._set_test_result_data(result.failures, TestStatus.fail) self._set_test_result_data(result.errors, TestStatus.error) self._set_test_result_data(result.skipped, TestStatus.skipped) for test in result.successes: node = self.node_lookup[str(test)] index = self.get_index_of_node(node) self.setData(index, "Test Passed", QtCore.Qt.ToolTipRole) self.setData(index, TestStatus.success, QtCore.Qt.DecorationRole) def _set_test_result_data(self, test_list, status): for test, reason in test_list: node = self.node_lookup[str(test)] index = self.get_index_of_node(node) self.setData(index, reason, QtCore.Qt.ToolTipRole) self.setData(index, status, QtCore.Qt.DecorationRole) # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # class RollbackImporter(object): """Used to remove imported modules from the module list. This allows tests to be rerun after code updates without doing any reloads. Original idea from: http://pyunit.sourceforge.net/notes/reloading.html Usage: def run_tests(self): if self.rollback_importer: self.rollback_importer.uninstall() self.rollback_importer = RollbackImporter() self.load_and_execute_tests() """ def __init__(self): """Creates an instance and installs as the global importer.""" self.previous_modules = set(sys.modules.keys()) def uninstall(self): for modname in sys.modules.keys(): if modname not in self.previous_modules: # Force reload when modname next imported del sys.modules[modname]