Index: /trunk/src/VBox/ValidationKit/testmanager/core/db.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/db.py	(revision 61283)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/db.py	(revision 61284)
@@ -332,8 +332,11 @@
         but collect data for traceback.
         """
-        if aoArgs is None:
-            aoArgs = list();
-
-        sBound = oCursor.mogrify(unicode(sOperation), aoArgs);
+        if aoArgs is not None:
+            sBound = oCursor.mogrify(unicode(sOperation), aoArgs);
+        elif sOperation.find('%') < 0:
+            sBound = oCursor.mogrify(unicode(sOperation), list());
+        else:
+            sBound = unicode(sOperation);
+
         if sys.version_info[0] >= 3 and not isinstance(sBound, str):
             sBound = sBound.decode('utf-8');
Index: /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61283)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61284)
@@ -145,4 +145,5 @@
         ModelLogicBase.__init__(self, oDb)
         self.ahCache = None;
+        self.ahCacheNameAndCat = None;
         self.oCategoryLogic = None;
         self.oUserAccountLogic = None;
@@ -436,4 +437,56 @@
         return oEntry;
 
+
+    def cachedLookupByNameAndCategory(self, sName, sCategory):
+        """
+        Looks up a failure reason by it's name and category.
+
+        Should the request be ambigiuos, we'll return the oldest one.
+
+        Returns a shared FailureReasonData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.ahCacheNameAndCat is None:
+            self.ahCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
+        sKey = '%s:::%s' % (sName, sCategory,);
+        oEntry = self.ahCacheNameAndCat.get(sKey, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     FailureReasons,\n'
+                              '         FailureCategories\n'
+                              'WHERE    FailureReasons.sShort            = %s\n'
+                              '     AND FailureReasons.tsExpire          = \'infinity\'::TIMESTAMP\n'
+                              '     AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory '
+                              '     AND FailureCategories.sShort         = %s\n'
+                              '     AND FailureCategories.tsExpire       = \'infinity\'::TIMESTAMP\n'
+                              'ORDER BY FailureReasons.tsEffective\n'
+                              , ( sName, sCategory));
+            if self._oDb.getRowCount() == 0:
+                sLikeSucks = self._oDb.formatBindArgs(
+                                  'SELECT   *\n'
+                                  'FROM     FailureReasons,\n'
+                                  '         FailureCategories\n'
+                                  'WHERE    (   FailureReasons.sShort    ILIKE @@@@@@@! %s !@@@@@@@\n'
+                                  '          OR FailureReasons.sFull     ILIKE @@@@@@@! %s !@@@@@@@)\n'
+                                  '     AND FailureCategories.tsExpire       = \'infinity\'::TIMESTAMP\n'
+                                  '     AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
+                                  '     AND (   FailureCategories.sShort     = %s\n'
+                                  '          OR FailureCategories.sFull      = %s)\n'
+                                  '     AND FailureReasons.tsExpire          = \'infinity\'::TIMESTAMP\n'
+                                  'ORDER BY FailureReasons.tsEffective\n'
+                                  , ( sName, sName, sCategory, sCategory ));
+                sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\'');
+                self._oDb.execute(sLikeSucks);
+            if self._oDb.getRowCount() > 0:
+                self._ensureCachesPresent();
+                oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
+                                                               self.oUserAccountLogic);
+                self.ahCacheNameAndCat[sKey] = oEntry;
+                if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort:
+                    sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,);
+                    self.ahCacheNameAndCat[sKey2] = oEntry;
+        return oEntry;
+
+
     #
     # Helpers.
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testcase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testcase.py	(revision 61283)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testcase.py	(revision 61284)
@@ -929,4 +929,8 @@
                         dErrors[self.ksParam_aoDepTestCases]   = 'Depending on itself!';
         return dErrors;
+
+
+
+
 
 class TestCaseLogic(ModelLogicBase):
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresultfailures.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresultfailures.py	(revision 61283)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresultfailures.py	(revision 61284)
@@ -103,4 +103,19 @@
         assert len(aoRow) == self.kcDbColumns;
         return self.initFromDbRow(aoRow);
+
+    def initFromValues(self, idTestResult, idFailureReason, uidAuthor,
+                       tsExpire = None, tsEffective = None, idTestSet = None, sComment = None):
+        """
+        Initialize from values.
+        """
+        self.idTestResult       = idTestResult;
+        self.tsEffective        = tsEffective;
+        self.tsExpire           = tsExpire;
+        self.uidAuthor          = uidAuthor;
+        self.idTestSet          = idTestSet;
+        self.idFailureReason    = idFailureReason;
+        self.sComment           = sComment;
+        return self;
+
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61283)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61284)
@@ -189,4 +189,22 @@
         self.sName = aoRow[9];
         return self;
+
+    def deepCountErrorContributers(self):
+        """
+        Counts how many test result instances actually contributed to cErrors.
+        """
+
+        # Check each child (if any).
+        cChanges = 0;
+        cChildErrors = 0;
+        for oChild in self.aoChildren:
+            cChanges += oChild.deepCountErrorContributers();
+            cChildErrors += oChild.cErrors;
+
+        # Did we contribute as well?
+        if self.cErrors != cChildErrors:
+            assert self.cErrors >= cChildErrors;
+            cChanges += 1;
+        return cChanges;
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testset.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testset.py	(revision 61283)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testset.py	(revision 61284)
@@ -207,5 +207,5 @@
                     return (None, 'Error opening "%s" inside "%s": %s' % (sFilename, sFile2, oXcpt2), None);
                 except Exception as oXcpt3:
-                    return (None, 'Aa! Megami-sama! %s; %s; %s' % (oXcpt1, oXcpt2, oXcpt3,), None);
+                    return (None, 'OMG! %s; %s; %s' % (oXcpt1, oXcpt2, oXcpt3,), None);
         return (None, 'Code not reachable!', None);
 
@@ -658,5 +658,5 @@
         return [aoRow[0] for aoRow in self._oDb.fetchAll()];
 
-    def fetchResultForTestBox(self, idTestBox, cHoursBack = 2, tsNow = None):
+    def fetchSetsForTestBox(self, idTestBox, cHoursBack = 2, tsNow = None):
         """
         Fetches the TestSet rows for idTestBox for the given period (tsDone), w/o running ones.
@@ -676,4 +676,33 @@
         return self._dbRowsToModelDataList(TestSetData);
 
+    def fetchFailedSetsWithoutReason(self, cHoursBack = 2, tsNow = None):
+        """
+        Fetches the TestSet failure rows without any currently (CURRENT_TIMESTAMP
+        not tsNow) assigned failure reason.
+
+        Returns list of TestSetData sorted by tsDone in descending order.
+
+        Note! Includes bad-testbox sets too as it can be useful to analyze these
+              too even if we normally count them in the 'skipped' category.
+        """
+        if tsNow is None:
+            tsNow = self._oDb.getCurrentTimestamp();
+        self._oDb.execute('SELECT TestSets.*\n'
+                          'FROM   TestSets\n'
+                          '       LEFT OUTER JOIN TestResultFailures\n'
+                          '                    ON TestResultFailures.idTestSet = TestSets.idTestSet\n'
+                          '                   AND TestResultFailures.tsExpire  = \'infinity\'::TIMESTAMP\n'
+                          'WHERE  TestSets.tsDone IS NOT NULL\n'
+                          '   AND TestSets.enmStatus        IN ( %s, %s, %s, %s )\n'
+                          '   AND TestSets.tsDone           <= %s\n'
+                          '   AND TestSets.tsDone            > (%s - interval \'%s hours\')\n'
+                          '   AND TestResultFailures.idTestSet IS NULL\n'
+                          'ORDER by tsDone DESC\n'
+                          , ( TestSetData.ksTestStatus_Failure, TestSetData.ksTestStatus_TimedOut,
+                              TestSetData.ksTestStatus_Rebooted, TestSetData.ksTestStatus_BadTestBox,
+                              tsNow,
+                              tsNow, cHoursBack,));
+        return self._dbRowsToModelDataList(TestSetData);
+
 
 
