Index: trunk/src/VBox/ValidationKit/testmanager/core/db.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/core/db.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/core/db.py	(revision 61217)
@@ -33,7 +33,7 @@
 import datetime;
 import os;
+import sys;
 import psycopg2;
 import psycopg2.extensions;
-import sys;
 
 # Validation Kit imports.
@@ -222,4 +222,7 @@
             oSrvGlue.registerDebugInfoCallback(self.debugInfoCallback);
 
+        # Object caches (used by database logic classes).
+        self.ddCaches = dict();
+
     def isAutoCommitting(self):
         """ Work around missing autocommit attribute in older versions."""
@@ -528,4 +531,16 @@
         oCursor = self._oConn.cursor();
         return TMDatabaseCursor(self, oCursor);
+
+    #
+    # Cache support.
+    #
+    def getCache(self, sType):
+        """ Returns the cache dictionary for this data type. """
+        dRet = self.ddCaches.get(sType, None);
+        if dRet is None:
+            dRet = dict();
+            self.ddCaches[sType] = dRet;
+        return dRet;
+
 
     #
Index: trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py	(revision 61217)
@@ -87,4 +87,8 @@
     Failure Category logic.
     """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.ahCache = None;
 
     def fetchForListing(self, iStart, cMaxRows, tsNow):
@@ -193,6 +197,5 @@
                           (idFailureCategory,))
         for iFailureReasonId in self._oDb.fetchAll():
-            FailureReasonLogic(self._oDb).remove(
-                uidAuthor, iFailureReasonId, fNeedCommit=False)
+            FailureReasonLogic(self._oDb).remove(uidAuthor, iFailureReasonId, fNeedCommit = False)
 
         self._oDb.execute('UPDATE FailureCategories\n'
@@ -238,2 +241,37 @@
 
         return True
+
+    def cachedLookup(self, idFailureCategory):
+        """
+        Looks up the most recent FailureCategoryData object for idFailureCategory
+        via an object cache.
+
+        Returns a shared FailureCategoryData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.ahCache is None:
+            self.ahCache = self._oDb.getCache('FailureCategory');
+
+        oEntry = self.ahCache.get(idFailureCategory, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     FailureCategories\n'
+                              'WHERE    idFailureCategory = %s\n'
+                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
+                              , (idFailureCategory, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     FailureCategories\n'
+                                  'WHERE    idFailureCategory = %s\n'
+                                  'ORDER BY tsExpire\n'
+                                  'LIMIT 1\n'
+                                  , (idFailureCategory, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureCategory));
+
+            if self._oDb.getRowCount() == 1:
+                oEntry = FailureCategoryData().initFromDbRow(self._oDb.fetchOne());
+                self.ahCache[idFailureCategory] = oEntry;
+        return oEntry;
+
Index: trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61217)
@@ -31,5 +31,7 @@
 
 # Validation Kit imports.
-from testmanager.core.base          import ModelDataBase, ModelLogicBase, TMExceptionBase
+from testmanager.core.base              import ModelDataBase, ModelLogicBase, TMExceptionBase
+from testmanager.core.useraccount       import UserAccountLogic;
+
 
 
@@ -92,5 +94,29 @@
         self.asUrls            = aoRow[8]
 
-        return self
+        return self;
+
+
+class FailureReasonDataEx(FailureReasonData):
+    """
+    Failure Reason Data, extended version that includes the category.
+    """
+
+    def __init__(self):
+        FailureReasonData.__init__(self);
+        self.oCategory  = None;
+        self.oAuthor    = None;
+
+    def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
+        """
+        Re-initializes the data with a row from a SELECT * FROM FailureReasons.
+
+        Returns self. Raises exception if the row is None or otherwise invalid.
+        """
+
+        self.initFromDbRow(aoRow);
+        self.oCategory  = oCategoryLogic.cachedLookup(self.idFailureCategory);
+        self.oAuthor    = oUserAccountLogic.cachedLookup(self.uidAuthor);
+
+        return self;
 
 
@@ -99,4 +125,10 @@
     Failure Reason logic.
     """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.ahCache = None;
+        self.oCategoryLogic = None;
+        self.oUserAccountLogic = None;
 
     def fetchForListing(self, iStart, cMaxRows, tsNow):
@@ -129,5 +161,5 @@
         return aoRows
 
-    def fetchForCombo(self, tsEffective = None):
+    def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
         """
         Gets the list of Failure Reasons for a combo box.
@@ -136,16 +168,24 @@
         """
         if tsEffective is None:
-            self._oDb.execute('SELECT   idFailureReason, sShort, sFull\n'
-                              'FROM     FailureReasons\n'
-                              'WHERE    tsExpire = \'infinity\'::TIMESTAMP\n'
-                              'ORDER BY sShort')
+            self._oDb.execute('SELECT   fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
+                              'FROM     FailureReasons fr,\n'
+                              '         FailureCategories fc\n'
+                              'WHERE    fr.idFailureCategory = fc.idFailureCategory\n'
+                              '  AND    fr.tsExpire = \'infinity\'::TIMESTAMP\n'
+                              '  AND    fc.tsExpire = \'infinity\'::TIMESTAMP\n'
+                              'ORDER BY sComboText')
         else:
-            self._oDb.execute('SELECT   idFailureReason, sShort, sFull\n'
-                              'FROM     FailureReasons\n'
-                              'WHERE    tsExpire     > %s\n'
-                              '     AND tsEffective <= %s\n'
-                              'ORDER BY sShort'
-                              , (tsEffective, tsEffective))
-        return self._oDb.fetchAll()
+            self._oDb.execute('SELECT   fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
+                              'FROM     FailureReasons fr,\n'
+                              '         FailureCategories fc\n'
+                              'WHERE    fr.idFailureCategory = fc.idFailureCategory\n'
+                              '  AND    fr.tsExpire     > %s\n'
+                              '  AND    fr.tsEffective <= %s\n'
+                              '  AND    fc.tsExpire     > %s\n'
+                              '  AND    fc.tsEffective <= %s\n'
+                              'ORDER BY sComboText'
+                              , (tsEffective, tsEffective, tsEffective, tsEffective));
+        aoRows = self._oDb.fetchAll();
+        return [(-1, sFirstEntry, '')] + aoRows;
 
     def getById(self, idFailureReason):
@@ -305,2 +345,42 @@
 
         return True
+
+    def cachedLookup(self, idFailureReason):
+        """
+        Looks up the most recent FailureReasonDataEx object for uid idFailureReason
+        an object cache.
+
+        Returns a shared FailureReasonData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.ahCache is None:
+            self.ahCache = self._oDb.getCache('FailureReasonDataEx');
+        oEntry = self.ahCache.get(idFailureReason, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     FailureReasons\n'
+                              'WHERE    idFailureReason = %s\n'
+                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
+                              , (idFailureReason, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     FailureReasons\n'
+                                  'WHERE    idFailureReason = %s\n'
+                                  'ORDER BY tsExpire\n'
+                                  'LIMIT 1\n'
+                                  , (idFailureReason, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
+
+            if self._oDb.getRowCount() == 1:
+                if self.oCategoryLogic is None:
+                    from testmanager.core.failurecategory import FailureCategoryLogic;
+                    self.oCategoryLogic = FailureCategoryLogic(self._oDb);
+                if self.oUserAccountLogic is None:
+                    self.oUserAccountLogic = UserAccountLogic(self._oDb);
+                oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
+                                                               self.oUserAccountLogic);
+                self.ahCache[idFailureReason] = oEntry;
+        return oEntry;
+
Index: trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61217)
@@ -38,10 +38,12 @@
 from testmanager                    import config;
 from testmanager.core.base          import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, TMTooManyRows;
-from testmanager.core.testgroup     import TestGroupData
-from testmanager.core.build         import BuildDataEx
-from testmanager.core.testbox       import TestBoxData
-from testmanager.core.testcase      import TestCaseData
-from testmanager.core.schedgroup    import SchedGroupData
+from testmanager.core.testgroup     import TestGroupData;
+from testmanager.core.build         import BuildDataEx;
+from testmanager.core.failurereason import FailureReasonLogic;
+from testmanager.core.testbox       import TestBoxData;
+from testmanager.core.testcase      import TestCaseData;
+from testmanager.core.schedgroup    import SchedGroupData;
 from testmanager.core.systemlog     import SystemLogData, SystemLogLogic;
+from testmanager.core.useraccount   import UserAccountLogic;
 
 
@@ -143,7 +145,8 @@
 
         self.aoChildren = [];   # TestResultDataEx;
-        self.aoValues   = [];   # TestResultValue;
-        self.aoMsgs     = [];   # TestResultMsg;
-        self.aoFiles    = [];   # TestResultFile;
+        self.aoValues   = [];   # TestResultValueDataEx;
+        self.aoMsgs     = [];   # TestResultMsgDataEx;
+        self.aoFiles    = [];   # TestResultFileDataEx;
+        self.oReason    = None; # TestResultReasonDataEx;
 
     def initFromDbRow(self, aoRow):
@@ -163,4 +166,5 @@
         self.aoMsgs     = [];
         self.aoFiles    = [];
+        self.oReason    = None;
 
         TestResultData.initFromDbRow(self, aoRow);
@@ -406,4 +410,88 @@
             return '%s; charset=utf-8' % (self.sMime,);
         return self.sMime;
+
+
+class TestResultFailureData(ModelDataBase):
+    """
+    Test result failure reason data.
+    """
+
+    ksIdAttr                    = 'idTestResult';
+
+    ksParam_idTestResult        = 'TestResultFailure_idTestResult';
+    ksParam_tsEffective         = 'TestResultFailure_tsEffective';
+    ksParam_tsExpire            = 'TestResultFailure_tsExpire';
+    ksParam_uidAuthor           = 'TestResultFailure_uidAuthor';
+    ksParam_idFailureReason     = 'TestResultFailure_idFailureReason';
+    ksParam_sComment            = 'TestResultFailure_sComment';
+
+    kasAllowNullAttributes      = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment' ];
+
+    def __init__(self):
+        ModelDataBase.__init__(self)
+        self.idTestResult       = None;
+        self.tsEffective        = None;
+        self.tsExpire           = None;
+        self.uidAuthor          = None;
+        self.idFailureReason    = None;
+        self.sComment           = None;
+
+    def initFromDbRow(self, aoRow):
+        """
+        Reinitialize from a SELECT * FROM TestResultFailures.
+        Return self. Raises exception if no row.
+        """
+        if aoRow is None:
+            raise TMExceptionBase('Test result file record not found.')
+
+        self.idTestResult       = aoRow[0];
+        self.tsEffective        = aoRow[1];
+        self.tsExpire           = aoRow[2];
+        self.uidAuthor          = aoRow[3];
+        self.idFailureReason    = aoRow[4];
+        self.sComment           = aoRow[5];
+        return self;
+
+    def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
+        """
+        Initialize the object from the database.
+        """
+        oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+                                                       'SELECT *\n'
+                                                       'FROM   TestResultFailures\n'
+                                                       'WHERE  idTestResult = %s\n'
+                                                       , ( idTestResult,), tsNow, sPeriodBack));
+        aoRow = oDb.fetchOne()
+        if aoRow is None:
+            raise TMExceptionBase('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack));
+        return self.initFromDbRow(aoRow);
+
+
+class TestResultFailureDataEx(TestResultFailureData):
+    """
+    Extends TestResultFailureData by resolving reasons and user.
+    """
+
+    def __init__(self):
+        TestResultFailureData.__init__(self);
+        self.oFailureReason     = None;
+        self.oAuthor            = None;
+
+    def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
+        """
+        Reinitialize from a query like this:
+            SELECT   TestResultFiles.*,
+                     StrTabFile.sValue AS sFile,
+                     StrTabDesc.sValue AS sDescription
+                     StrTabKind.sValue AS sKind,
+                     StrTabMime.sValue AS sMime,
+            FROM ...
+
+        Return self. Raises exception if no row.
+        """
+        self.initFromDbRow(aoRow);
+        self.oFailureReason = oFailureReasonLogic.cachedLookup(self.idFailureReason);
+        self.oAuthor        = oUserAccountLogic.cachedLookup(self.uidAuthor);
+        return self;
 
 
@@ -453,5 +541,10 @@
         self.iRevisionTestSuite      = None;
 
-    def initFromDbRow(self, aoRow):
+        self.oFailureReason          = None;
+        self.oFailureReasonAssigner  = None;
+        self.tsFailureReasonAssigned = None;
+        self.sFailureReasonComment   = None;
+
+    def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
         """
         Reinitialize from a database query.
@@ -497,4 +590,13 @@
         self.iRevisionTestSuite      = aoRow[29];
 
+        self.oFailureReason          = None;
+        if aoRow[30] is not None:
+            self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[30]);
+        self.oFailureReasonAssigner  = None;
+        if aoRow[31] is not None:
+            self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[31]);
+        self.tsFailureReasonAssigned = aoRow[32];
+        self.sFailureReasonComment   = aoRow[33];
+
         return self
 
@@ -503,4 +605,5 @@
     """Hanging offence committed by test case."""
     pass;
+
 
 class TestResultLogic(ModelLogicBase): # pylint: disable=R0903
@@ -534,4 +637,5 @@
     ksResultsSortByTestBoxCpuFeatures   = 'ResultsSortByTestBoxCpuFeatures';
     ksResultsSortByTestCaseName         = 'ResultsSortByTestCaseName';
+    ksResultsSortByFailureReason        = 'ResultsSortByFailureReason';
     kasResultsSortBy = {
         ksResultsSortByRunningAndStart,
@@ -547,4 +651,5 @@
         ksResultsSortByTestBoxCpuFeatures,
         ksResultsSortByTestCaseName,
+        ksResultsSortByFailureReason,
     };
     ## Used by the WUI for generating the drop down.
@@ -562,4 +667,5 @@
         ( ksResultsSortByTestBoxCpuFeatures,    'CPU Features' ),
         ( ksResultsSortByTestCaseName,          'Test Case Name' ),
+        ( ksResultsSortByFailureReason,         'Failure Reason' ),
     );
     ## @}
@@ -629,4 +735,8 @@
             ' TestCases.sName',
             ''  ),
+        ksResultsSortByFailureReason: (
+            '', '',
+            'sSortByFailureReason ASC',
+            ', FailureReasons.sShort AS sSortByFailureReason' ),
     };
 
@@ -668,4 +778,9 @@
 
 
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.oFailureReasonLogic = None;
+        self.oUserAccountLogic   = None;
+
     def _getTimePeriodQueryPart(self, tsNow, sInterval):
         """
@@ -752,9 +867,21 @@
                   '       TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
                   '       TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
+                  '       TestResultFailures.idFailureReason as idFailureReason,\n' \
+                  '       TestResultFailures.uidAuthor as uidFailureReasonAssigner,\n' \
+                  '       TestResultFailures.tsEffective as tsFailureReasonAssigned,\n' \
+                  '       TestResultFailures.sComment as sFailureReasonComment,\n' \
                   '       (TestSets.tsDone IS NULL) SortRunningFirst' + sSortingColumns + '\n' \
                   'FROM   BuildCategories,\n' \
                   '       Builds,\n' \
                   '       TestBoxes,\n' \
-                  '       TestResults,\n' \
+                  '       TestResults LEFT OUTER JOIN TestResultFailures\n' \
+                  '            ON TestResults.idTestResult    = TestResultFailures.idTestResult\n' \
+                  '           AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
+        if sSortingOrderBy.find('FailureReason') >= 0:
+            sQuery += '\n' \
+                      '       LEFT OUTER JOIN FailureReasons\n' \
+                      '            ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
+                      '           AND FailureReasons.tsExpire            = \'infinity\'::TIMESTAMP';
+        sQuery += ',\n'\
                   '       TestCases,\n' \
                   '       TestCaseArgs,\n' \
@@ -780,5 +907,5 @@
             sQuery += sSortingWhere.replace(' AND ', '            AND ');
         sQuery += '          ORDER BY ';
-        if sSortingOrderBy is not None:
+        if sSortingOrderBy is not None and sSortingOrderBy.find('FailureReason') < 0:
             sQuery += sSortingOrderBy + ',\n                ';
         sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
@@ -807,7 +934,12 @@
         self._oDb.execute(sQuery);
 
+        if self.oFailureReasonLogic is None:
+            self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
+        if self.oUserAccountLogic is None:
+            self.oUserAccountLogic = UserAccountLogic(self._oDb);
+
         aoRows = [];
         for aoRow in self._oDb.fetchAll():
-            aoRows.append(TestResultListingData().initFromDbRow(aoRow))
+            aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic));
 
         return aoRows
@@ -1014,5 +1146,8 @@
             '         EXISTS ( SELECT idTestResultFile\n'
             '           FROM   TestResultFiles\n'
-            '           WHERE  TestResultFiles.idTestResult  = TestResults.idTestResult ) AS fHasFiles\n'
+            '           WHERE  TestResultFiles.idTestResult  = TestResults.idTestResult ) AS fHasFiles,\n'
+            '         EXISTS ( SELECT idTestResult\n'
+            '           FROM   TestResultFailures\n'
+            '           WHERE  TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n'
             'FROM     TestResults, TestResultStrTab\n'
             'WHERE    TestResults.idTestSet = %s\n'
@@ -1038,5 +1173,5 @@
             raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
                                                % (oRoot.idTestResult, oRoot.idTestResultParent));
-        self._fetchResultTreeNodeExtras(oRoot, aoRow[-3], aoRow[-2], aoRow[-1]);
+        self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
 
         # The chilren (if any).
@@ -1046,5 +1181,5 @@
             aoRow = aaoRows[iRow];
             oCur = TestResultDataEx().initFromDbRow(aoRow);
-            self._fetchResultTreeNodeExtras(oCur, aoRow[-3], aoRow[-2], aoRow[-1]);
+            self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
 
             # Figure out and vet the parent.
@@ -1065,12 +1200,13 @@
         return (oRoot, dLookup);
 
-    def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles):
+    def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons):
         """
         fetchResultTree worker that fetches values, message and files for the
         specified node.
         """
-        assert(oCurNode.aoValues == []);
-        assert(oCurNode.aoMsgs   == []);
-        assert(oCurNode.aoFiles  == []);
+        assert(oCurNode.aoValues  == []);
+        assert(oCurNode.aoMsgs    == []);
+        assert(oCurNode.aoFiles   == []);
+        assert(oCurNode.oReason is None);
 
         if fHasValues:
@@ -1117,4 +1253,18 @@
                 oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
 
+        if fHasReasons or True:
+            if self.oFailureReasonLogic is None:
+                self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
+            if self.oUserAccountLogic is None:
+                self.oUserAccountLogic = UserAccountLogic(self._oDb);
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     TestResultFailures\n'
+                              'WHERE    idTestResult = %s\n'
+                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
+                              , ( oCurNode.idTestResult, ));
+            if self._oDb.getRowCount() > 0:
+                oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic,
+                                                                             self.oUserAccountLogic);
+
         return True;
 
@@ -1184,5 +1334,5 @@
         """Returns a string rep of the stack."""
         sRet = '';
-        for i in range(len(aoStack)):
+        for i, _ in enumerate(aoStack):
             sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
         return sRet;
@@ -1202,5 +1352,5 @@
             aoStack.append(TestResultData().initFromDbRow(aoRow));
 
-        for i in range(len(aoStack)):
+        for i, _ in enumerate(aoStack):
             assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
 
@@ -1473,5 +1623,5 @@
             if sAttr in dAttribs:
                 try:
-                    _ = long(dAttribs[sAttr]);
+                    _ = long(dAttribs[sAttr]);  # pylint: disable=R0204
                 except:
                     return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
@@ -1761,4 +1911,132 @@
 
 
+
+class TestResultFailureLogic(ModelLogicBase): # pylint: disable=R0903
+    """
+    Test result failure reason logic.
+    """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+
+    def getById(self, idTestResult):
+        """Get Test result failure reason data by idTestResult"""
+
+        self._oDb.execute('SELECT   *\n'
+                          'FROM     TestResultFailures\n'
+                          'WHERE    tsExpire   = \'infinity\'::timestamp\n'
+                          '  AND    idTestResult = %s;', (idTestResult,))
+        aRows = self._oDb.fetchAll()
+        if len(aRows) not in (0, 1):
+            raise self._oDb.integrityException(
+                'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
+        try:
+            return TestResultFailureData().initFromDbRow(aRows[0])
+        except IndexError:
+            return None
+
+    def addEntry(self, oData, uidAuthor, fCommit = False):
+        """
+        Add a test result failure reason record.
+        """
+
+        # Check if it exist first (we're adding, not editing, collisions not allowed).
+        oOldData = self.getById(oData.idTestResult);
+        if oOldData is not None:
+            raise TMExceptionBase('TestResult %d already have a failure reason associated with it:'
+                                  '%s\n'
+                                  'Perhaps someone else beat you to it? Or did you try resubmit?'
+                                  % (oData.idTestResult, oOldData));
+
+        #
+        # Add record.
+        #
+        self._readdEntry(uidAuthor, oData);
+        self._oDb.maybeCommit(fCommit);
+        return True;
+
+    def editEntry(self, oData, uidAuthor, fCommit = False):
+        """
+        Modifies a test result failure reason.
+        """
+
+        #
+        # Validate inputs and read in the old(/current) data.
+        #
+        assert isinstance(oData, TestResultFailureData);
+        dErrors = oData.validateAndConvert(self._oDb);
+        if len(dErrors) > 0:
+            raise TMExceptionBase('editEntry invalid input: %s' % (dErrors,));
+
+        oOldData = self.getById(oData.idTestResult)
+
+        #
+        # Update the data that needs updating.
+        #
+        if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+            self._historizeEntry(oData.idTestResult);
+            self._readdEntry(uidAuthor, oData);
+        self._oDb.maybeCommit(fCommit);
+        return True;
+
+
+    def removeEntry(self, uidAuthor, idTestResult, fCascade = False, fCommit = False):
+        """
+        Deletes a test result failure reason.
+        """
+        _ = fCascade; # Not applicable.
+
+        oData = self.getById(idTestResult)
+        (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+        if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:
+            self._historizeEntry(idTestResult, tsCurMinusOne);
+            self._readdEntry(uidAuthor, oData, tsCurMinusOne);
+            self._historizeEntry(idTestResult);
+        self._oDb.execute('UPDATE   TestResultFaillures\n'
+                          'SET      tsExpire       = CURRENT_TIMESTAMP\n'
+                          'WHERE    idTestResult   = %s\n'
+                          '     AND tsExpire       = \'infinity\'::TIMESTAMP\n'
+                          , (idTestResult,));
+        self._oDb.maybeCommit(fCommit);
+        return True;
+
+    #
+    # Helpers.
+    #
+
+    def _readdEntry(self, uidAuthor, oData, tsEffective = None):
+        """
+        Re-adds the TestResultFailure entry. Used by addEntry, editEntry and removeEntry.
+        """
+        if tsEffective is None:
+            tsEffective = self._oDb.getCurrentTimestamp();
+        self._oDb.execute('INSERT INTO TestResultFailures (\n'
+                          '         uidAuthor,\n'
+                          '         tsEffective,\n'
+                          '         idTestResult,\n'
+                          '         idFailureReason,\n'
+                          '         sComment)\n'
+                          'VALUES (%s, %s, %s, %s, %s)\n'
+                          , ( uidAuthor,
+                              tsEffective,
+                              oData.idTestResult,
+                              oData.idFailureReason,
+                              oData.sComment,) );
+        return True;
+
+
+    def _historizeEntry(self, idTestResult, tsExpire = None):
+        """ Historizes the current entry. """
+        if tsExpire is None:
+            tsExpire = self._oDb.getCurrentTimestamp();
+        self._oDb.execute('UPDATE TestResultFailures\n'
+                          'SET    tsExpire   = %s\n'
+                          'WHERE  idTestResult = %s\n'
+                          '   AND tsExpire     = \'infinity\'::TIMESTAMP\n'
+                          , (tsExpire, idTestResult,));
+        return True;
+
+
+
 #
 # Unit testing.
Index: trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py	(revision 61217)
@@ -116,6 +116,10 @@
 class UserAccountLogic(ModelLogicBase):
     """
-    SystemLog logic.
-    """
+    User account logic (for the Users table).
+    """
+
+    def __init__(self, oDb):
+        ModelLogicBase.__init__(self, oDb)
+        self.ahCache = None;
 
     def fetchForListing(self, iStart, cMaxRows, tsNow):
@@ -221,4 +225,37 @@
         return UserAccountData().initFromDbRow(self._oDb.fetchOne());
 
+    def cachedLookup(self, uid):
+        """
+        Looks up the current UserAccountData object for uid via an object cache.
+
+        Returns a shared UserAccountData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.ahCache is None:
+            self.ahCache = self._oDb.getCache('UserAccount');
+
+        oUser = self.ahCache.get(uid, None);
+        if oUser is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     Users\n'
+                              'WHERE    uid = %s\n'
+                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
+                              , (uid, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     Users\n'
+                                  'WHERE    uid = %s\n'
+                                  'ORDER BY tsExpire\n'
+                                  'LIMIT 1\n'
+                                  , (uid, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), uid));
+
+            if self._oDb.getRowCount() == 1:
+                oUser = UserAccountData().initFromDbRow(self._oDb.fetchOne());
+                self.ahCache[uid] = oUser;
+        return oUser;
+
     def resolveChangeLogAuthors(self, aoEntries):
         """
@@ -229,15 +266,8 @@
         Raises exception on DB error.
         """
-        ahCache = dict();
         for oEntry in aoEntries:
-            oEntry.sAuthor = ahCache.get(oEntry.uidAuthor, None);
-            if oEntry.sAuthor is None and oEntry.uidAuthor is not None:
-                try:
-                    oUser = UserAccountData().initFromDbWithId(self._oDb, oEntry.uidAuthor, oEntry.tsEffective);
-                except:
-                    pass;
-                else:
-                    ahCache[oEntry.uidAuthor] = oUser.sUsername;
-                    oEntry.sAuthor = oUser.sUsername;
+            oUser = self.cachedLookup(oEntry.uidAuthor)
+            if oUser is not None:
+                oEntry.sAuthor = oUser.sUsername;
         return aoEntries;
 
Index: trunk/src/VBox/ValidationKit/testmanager/db/Makefile.kmk
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/db/Makefile.kmk	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/db/Makefile.kmk	(revision 61217)
@@ -28,5 +28,5 @@
 # Need proper shell on windows.
 DEPTH = ../../../../..
-include $(KBUILD_PATH)/header.kmk
+#include $(KBUILD_PATH)/header.kmk
 
 
Index: trunk/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py	(revision 61217)
@@ -262,4 +262,37 @@
         oDb.commit();
 
+        # Correct sequences.
+        atSequences = [
+            ( 'UserIdSeq',              'Users',                'uid' ),
+            ( 'GlobalResourceIdSeq',    'GlobalResources',      'idGlobalRsrc' ),
+            ( 'BuildSourceIdSeq',       'BuildSources',         'idBuildSrc' ),
+            ( 'TestCaseIdSeq',          'TestCases',            'idTestCase' ),
+            ( 'TestCaseGenIdSeq',       'TestCases',            'idGenTestCase' ),
+            ( 'TestCaseArgsIdSeq',      'TestCaseArgs',         'idTestCaseArgs' ),
+            ( 'TestCaseArgsGenIdSeq',   'TestCaseArgs',         'idGenTestCaseArgs' ),
+            ( 'TestGroupIdSeq',         'TestGroups',           'idTestGroup' ),
+            ( 'SchedGroupIdSeq',        'SchedGroups',          'idSchedGroup' ),
+            ( 'TestBoxIdSeq',           'TestBoxes',            'idTestBox' ),
+            ( 'TestBoxGenIdSeq',        'TestBoxes',            'idGenTestBox' ),
+            ( 'FailureCategoryIdSeq',   'FailureCategories',    'idFailureCategory' ),
+            ( 'FailureReasonIdSeq',     'FailureReasons',       'idFailureReason' ),
+            ( 'BuildBlacklistIdSeq',    'BuildBlacklist',       'idBlacklisting' ),
+            ( 'BuildCategoryIdSeq',     'BuildCategories',      'idBuildCategory' ),
+            ( 'BuildIdSeq',             'Builds',               'idBuild' ),
+            ( 'TestResultStrTabIdSeq',  'TestResultStrTab',     'idStr' ),
+            ( 'TestResultIdSeq',        'TestResults',          'idTestResult' ),
+            ( 'TestResultValueIdSeq',   'TestResultValues',     'idTestResultValue' ),
+            ( 'TestResultFileId',       'TestResultFiles',      'idTestResultFile' ),
+            ( 'TestResultMsgIdSeq',     'TestResultMsgs',       'idTestResultMsg' ),
+            ( 'TestSetIdSeq',           'TestSets',             'idTestSet' ),
+            ( 'SchedQueueItemIdSeq',    'SchedQueues',          'idItem' ),
+        ];
+        for (sSeq, sTab, sCol) in atSequences:
+            oDb.execute('SELECT MAX(%s) FROM %s' % (sCol, sTab,));
+            idMax = oDb.fetchOne()[0];
+            print '%s: idMax=%s' % (sSeq, idMax);
+            if idMax is not None:
+                oDb.execute('SELECT setval(\'%s\', %s)' % (sSeq, idMax));
+
         # Last step.
         print 'Analyzing...'
Index: trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js	(revision 61217)
@@ -27,4 +27,10 @@
 
 
+/*********************************************************************************************************************************
+*   Global Variables                                                                                                             *
+*********************************************************************************************************************************/
+/** Same as WuiDispatcherBase.ksParamRedirectTo. */
+var g_ksParamRedirectTo = 'RedirectTo';
+
 
 /**
@@ -161,4 +167,34 @@
 {
     return getUnscaledElementWidth(document.getElementById(sElementId));
+}
+
+/**
+ * Gets the part of the URL needed for a RedirectTo parameter.
+ *
+ * @returns URL string.
+ */
+function getCurrentBrowerUrlPartForRedirectTo()
+{
+    var sWhere = window.location.href;
+    var offTmp;
+    var offPathKeep;
+
+    /* Find the end of that URL 'path' component. */
+    var offPathEnd = sWhere.indexOf('?');
+    if (offPathEnd < 0)
+        offPathEnd = sWhere.indexOf('#');
+    if (offPathEnd < 0)
+        offPathEnd = sWhere.length;
+
+    /* Go backwards from the end of the and find the start of the last component. */
+    offPathKeep = sWhere.lastIndexOf("/", offPathEnd);
+    offTmp = sWhere.lastIndexOf(":", offPathEnd);
+    if (offPathKeep < offTmp)
+        offPathKeep = offTmp;
+    offTmp = sWhere.lastIndexOf("\\", offPathEnd);
+    if (offPathKeep < offTmp)
+        offPathKeep = offTmp;
+
+    return sWhere.substring(offPathKeep + 1);
 }
 
@@ -266,4 +302,54 @@
 
 }
+
+/**
+ * Adds the RedirecTo field with the current URL to the form.
+ *
+ * This is a 'onsubmit' action.
+ *
+ * @returns Returns success indicator (true/false).
+ * @param   oForm               The form being submitted.
+ */
+function addRedirectToInputFieldWithCurrentUrl(oForm)
+{
+    /* Constant used here is duplicated in WuiDispatcherBase.ksParamRedirectTo */
+    return addHiddenInputFieldToForm(oForm, 'RedirectTo', getCurrentBrowerUrlPartForRedirectTo(), null);
+}
+
+/**
+ * Adds the RedirecTo parameter to the href of the given anchor.
+ *
+ * This is a 'onclick' action.
+ *
+ * @returns Returns success indicator (true/false).
+ * @param   oAnchor         The anchor element being clicked on.
+ */
+function addRedirectToAnchorHref(oAnchor)
+{
+    var sRedirectToParam = g_ksParamRedirectTo + '=' + encodeURIComponent(getCurrentBrowerUrlPartForRedirectTo());
+    var sHref = oAnchor.href;
+    if (sHref.indexOf(sRedirectToParam) < 0)
+    {
+        var sHash;
+        var offHash = sHref.indexOf('#');
+        if (offHash >= 0)
+            sHash = sHref.substring(offHash);
+        else
+        {
+            sHash   = '';
+            offHash = sHref.length;
+        }
+        sHref = sHref.substring(0, offHash)
+        if (sHref.indexOf('?') >= 0)
+            sHref += '&';
+        else
+            sHref += '?';
+        sHref += sRedirectToParam;
+        sHref += sHash;
+        oAnchor.href = sHref;
+    }
+    return true;
+}
+
 
 
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py	(revision 61217)
@@ -138,4 +138,5 @@
 
     ksActionFailureReasonList       = 'FailureReasonList'
+    ksActionFailureReasonDetails    = 'FailureReasonDetails'
     ksActionFailureReasonShowAdd    = 'FailureReasonShowAdd'
     ksActionFailureReasonShowEdit   = 'FailureReasonShowEdit'
@@ -365,8 +366,8 @@
                                                                 WuiAdminFailureReasonList)
 
-        d[self.ksActionFailureReasonShowAdd]    = lambda: self._actionGenericFormAdd(
-                                                                FailureReasonData,
-                                                                WuiAdminFailureReason)
-
+        d[self.ksActionFailureReasonDetails]    = lambda: self._actionGenericFormDetails(FailureReasonData,
+                                                                                         FailureReasonLogic,
+                                                                                         WuiAdminFailureReason,
+                                                                                         'idFailureReason');
         d[self.ksActionFailureReasonShowEdit]   = lambda: self._actionGenericFormEditL(
                                                                 FailureReasonLogic,
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 61217)
@@ -74,4 +74,7 @@
     ## The name of the effective date (timestamp) parameter.
     ksParamEffectiveDate = 'EffectiveDate';
+
+    ## The name of the redirect-to (test manager relative url) parameter.
+    ksParamRedirectTo    = 'RedirectTo';
 
     ## The name of the list-action parameter (WuiListContentWithActionBase).
@@ -546,4 +549,26 @@
         return str(oDate);
 
+    def getRedirectToParameter(self, sDefault = None):
+        """
+        Gets the special redirect to parameter if it exists, will Return default
+        if not, with None being a valid default.
+
+        Makes sure the it doesn't got offsite.
+        Raises exception if invalid.
+        """
+        if sDefault is not None or self.ksParamRedirectTo in self._dParams:
+            sValue = self.getStringParam(self.ksParamRedirectTo, sDefault = sDefault);
+            cch = sValue.find("?");
+            if cch < 0:
+                cch = sValue.find("#");
+                if cch < 0:
+                    cch = len(sValue);
+            for ch in (':', '/', '\\', '..'):
+                if sValue.find(ch, 0, cch) >= 0:
+                    raise WuiException('Invalid character (%c) in redirect-to url: %s' % (ch, sValue,));
+        else:
+            sValue = None;
+        return sValue;
+
 
     def _checkForUnknownParameters(self):
@@ -701,5 +726,5 @@
         return True;
 
-    def _actionGenericFormAdd(self, oDataType, oFormType):
+    def _actionGenericFormAdd(self, oDataType, oFormType, sRedirectTo = None):
         """
         Generic add something form display request handler.
@@ -709,7 +734,9 @@
         """
         oData = oDataType().initFromParams(oDisp = self, fStrict = False);
+        sRedirectTo = self.getRedirectToParameter(sRedirectTo);
         self._checkForUnknownParameters();
 
         oForm = oFormType(oData, oFormType.ksMode_Add, oDisp = self);
+        oForm.setRedirectTo(sRedirectTo);
         (self._sPageTitle, self._sPageBody) = oForm.showForm();
         return True
@@ -757,5 +784,5 @@
 
 
-    def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName):
+    def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName, sRedirectTo = None):
         """
         Generic edit something form display request handler.
@@ -766,9 +793,12 @@
         """
 
+        tsNow    = self.getEffectiveDateParam();
         idObject = self.getIntParam(sIdParamName, 0, 0x7ffffffe);
+        sRedirectTo = self.getRedirectToParameter(sRedirectTo);
         self._checkForUnknownParameters();
-        oData = oDataType().initFromDbWithId(self._oDb, idObject);
+        oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow = tsNow);
 
         oContent = oFormType(oData, oFormType.ksMode_Edit, oDisp = self);
+        oContent.setRedirectTo(sRedirectTo);
         (self._sPageTitle, self._sPageBody) = oContent.showForm();
         return True
@@ -850,4 +880,5 @@
         #
         oData = oDataType().initFromParams(oDisp = self, fStrict = fStrict);
+        sRedirectTo = self.getRedirectToParameter(sRedirectTo);
         self._checkForUnknownParameters();
         self._assertPostRequest();
@@ -864,4 +895,5 @@
                 self._oDb.rollback();
                 oForm = oFormType(oData, sMode, oDisp = self);
+                oForm.setRedirectTo(sRedirectTo);
                 sErrorMsg = str(oXcpt) if not config.g_kfDebugDbXcpt else '\n'.join(utils.getXcptInfo(4));
                 (self._sPageTitle, self._sPageBody) = oForm.showForm(sErrorMsg = sErrorMsg);
@@ -961,5 +993,5 @@
             self._sAction = self.ksActionDefault;
 
-        if self._sAction not in self._dDispatch:
+        if isinstance(self._sAction, list) or  self._sAction not in self._dDispatch:
             raise WuiException('Unknown action "%s" requested' % (self._sAction,));
 
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 61217)
@@ -190,7 +190,11 @@
 
     ## The text/symbol for a very short edit link.
-    ksShortEditLink    = u'\u270D'
+    ksShortEditLink        = u'\u270D'
+    ## HTML hex entity string for ksShortDetailsLink.
+    ksShortEditLinkHtml    = '&#x270d;'
     ## The text/symbol for a very short details link.
-    ksShortDetailsLink = u'\u2318'
+    ksShortDetailsLink     = u'\u2318'
+    ## HTML hex entity string for ksShortDetailsLink.
+    ksShortDetailsLinkHtml = '&#x2318;'
 
 
@@ -327,4 +331,5 @@
         if sSubmitAction is None and sMode != self.ksMode_Show:
             self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]);
+        self._sRedirectTo   = None;
 
 
@@ -469,4 +474,11 @@
                        '</div>\n';
         return sNavigation;
+
+    def setRedirectTo(self, sRedirectTo):
+        """
+        For setting the hidden redirect-to field.
+        """
+        self._sRedirectTo = sRedirectTo;
+        return True;
 
     def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True):
@@ -521,4 +533,6 @@
         try:
             self._populateForm(oForm, self._oData);
+            if self._sRedirectTo is not None:
+                oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo);
         except WuiException, oXcpt:
             sContent = unicode(oXcpt)
@@ -558,5 +572,5 @@
                 dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._oData.tsEffective;
                 dParams[getattr(self._oData, 'ksParam_' + self._oData.ksIdAttr)] = getattr(self._oData, self._oData.ksIdAttr);
-            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
+            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
             aoActions.append(WuiTmLink('Details', '', dParams));
 
@@ -629,5 +643,5 @@
         assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
 
-        for i in range(len(aoValues)):
+        for i, _ in enumerate(aoValues):
             if i < len(self._asColumnAttribs) and len(self._asColumnAttribs[i]) > 0:
                 sRow += u'    <td ' + self._asColumnAttribs[i] + '>';
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py	(revision 61217)
@@ -48,13 +48,21 @@
     ksItemsList = 'ksItemsList'
 
-    def __init__(self, sId, sAction, dErrors = None, fReadOnly = False):
+    ksOnSubmit_AddReturnToFieldWithCurrentUrl = '+AddReturnToFieldWithCurrentUrl+';
+
+    def __init__(self, sId, sAction, dErrors = None, fReadOnly = False, sOnSubmit = None):
         self._fFinalized = False;
         self._fReadOnly  = fReadOnly;
         self._dErrors    = dErrors if dErrors is not None else dict();
+
+        if sOnSubmit == self.ksOnSubmit_AddReturnToFieldWithCurrentUrl:
+            sOnSubmit = 'return addRedirectToInputFieldWithCurrentUrl(this)';
+        if sOnSubmit is None:   sOnSubmit = u'';
+        else:                   sOnSubmit = u' onsubmit=\"%s\"' % (escapeAttr(sOnSubmit),);
+
         self._sBody      = u'\n' \
                            u'<div id="%s" class="tmform">\n' \
-                           u'  <form action="%s" method="post">\n' \
+                           u'  <form action="%s" method="post"%s>\n' \
                            u'    <ul>\n' \
-                         % (sId, sAction);
+                         % (sId, sAction, sOnSubmit);
 
     def _add(self, sText):
@@ -70,5 +78,5 @@
         if sText.find('<br>') >= 0:
             asParts = sText.split('<br>');
-            for i in range(len(asParts)):
+            for i, _ in enumerate(asParts):
                 asParts[i] = escapeElem(asParts[i].strip());
             sText = '<br>\n'.join(asParts);
@@ -114,4 +122,16 @@
                          '    </li>\n'
                          % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeElem(str(sValue)) ));
+    #
+    # Non-input stuff.
+    #
+    def addNonText(self, sValue, sLabel, sPostHtml = ''):
+        """Adds a read-only text input."""
+        self._addLabel('non-text', sLabel, 'string');
+        if sValue is None: sValue = '';
+        return self._add('          <p>%s</p>%s\n'
+                         '        </div></div>\n'
+                         '      </li>\n'
+                         % (escapeElem(str(sValue)), sPostHtml ));
+
 
     #
@@ -124,4 +144,5 @@
         if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
         self._addLabel(sName, sLabel, sSubClass);
+        if sValue is None: sValue = '';
         return self._add('          <input name="%s" id="%s" type="text"%s value="%s">%s\n'
                          '        </div></div>\n'
@@ -133,4 +154,5 @@
         if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
         self._addLabel(sName, sLabel, sSubClass);
+        if sValue is None: sValue = '';
         return self._add('          <input name="%s" id="%s" type="text" readonly%s value="%s" class="tmform-input-readonly">%s\n'
                          '        </div></div>\n'
@@ -153,4 +175,5 @@
         if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
         self._addLabel(sName, sLabel, sSubClass)
+        if sValue is None: sValue = '';
         sNewValue = str(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
         return self._add('          <textarea name="%s" id="%s" %s>%s</textarea>\n'
@@ -163,4 +186,5 @@
         if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
         self._addLabel(sName, sLabel, sSubClass)
+        if sValue is None: sValue = '';
         sNewValue = str(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
         return self._add('          <textarea name="%s" id="%s" readonly %s>%s</textarea>\n'
@@ -213,8 +237,9 @@
         self._add('          <select name="%s" id="%s" class="tmform-combobox"%s>\n'
                   % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
+        sSelected = str(sSelected);
         for iValue, sText, _ in aoOptions:
             sValue = str(iValue);
             self._add('            <option value="%s"%s>%s</option>\n'
-                      % (escapeAttr(sValue), ' selected' if sValue == str(sSelected) else '',
+                      % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
                          escapeElem(sText)));
         return self._add('          </select>\n'
@@ -229,8 +254,9 @@
         self._add('          <select name="%s" id="%s" disabled class="tmform-combobox"%s>\n'
                   % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
+        sSelected = str(sSelected);
         for iValue, sText, _ in aoOptions:
             sValue = str(iValue);
             self._add('            <option value="%s"%s>%s</option>\n'
-                      % (escapeAttr(sValue), ' selected' if sValue == str(sSelected) else '',
+                      % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
                          escapeElem(sText)));
         return self._add('          </select>\n'
@@ -526,5 +552,5 @@
             dSubErrors = self._dErrors[sName];
 
-        for iVar in range(len(aoVariations)):
+        for iVar, _ in enumerate(aoVariations):
             oVar = copy.copy(aoVariations[iVar]);
             oVar.convertToParamNull();
@@ -623,10 +649,10 @@
         oDefMember = TestGroupMemberData();
         aoTestGroupMembers = list(aoTestGroupMembers); # Copy it so we can pop.
-        for iTestCase in range(len(aoAllTestCases)):
+        for iTestCase, _ in enumerate(aoAllTestCases):
             oTestCase = aoAllTestCases[iTestCase];
 
             # Is it a member?
             oMember = None;
-            for i in range(len(aoTestGroupMembers)):
+            for i, _ in enumerate(aoTestGroupMembers):
                 if aoTestGroupMembers[i].oTestCase.idTestCase == oTestCase.idTestCase:
                     oMember = aoTestGroupMembers.pop(i);
@@ -735,10 +761,10 @@
         oDefMember = SchedGroupMemberData();
         aoSchedGroupMembers = list(aoSchedGroupMembers); # Copy it so we can pop.
-        for iTestGroup in range(len(aoAllTestGroups)):
+        for iTestGroup, _ in enumerate(aoAllTestGroups):
             oTestGroup = aoAllTestGroups[iTestGroup];
 
             # Is it a member?
             oMember = None;
-            for i in range(len(aoSchedGroupMembers)):
+            for i, _ in enumerate(aoSchedGroupMembers):
                 if aoSchedGroupMembers[i].oTestGroup.idTestGroup == oTestGroup.idTestGroup:
                     oMember = aoSchedGroupMembers.pop(i);
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61217)
@@ -69,4 +69,9 @@
     ksActionResultsGroupedByTestCase    = 'ResultsGroupedByTestCase'
     ksActionTestResultDetails           = 'TestResultDetails'
+    ksActionTestResultFailureDetails    = 'TestResultFailureDetails'
+    ksActionTestResultFailureAdd        = 'TestResultFailureAdd'
+    ksActionTestResultFailureAddPost    = 'TestResultFailureAddPost'
+    ksActionTestResultFailureEdit       = 'TestResultFailureEdit'
+    ksActionTestResultFailureEditPost   = 'TestResultFailureEditPost'
     ksActionViewLog                     = 'ViewLog'
     ksActionGetFile                     = 'GetFile'
@@ -228,5 +233,11 @@
                                                                 WuiGroupedResultList)
 
-        d[self.ksActionTestResultDetails]          = self.actionTestResultDetails
+        d[self.ksActionTestResultDetails]          = self._actionTestResultDetails;
+
+        d[self.ksActionTestResultFailureAdd]       = self._actionTestResultFailureAdd;
+        d[self.ksActionTestResultFailureAddPost]   = self._actionTestResultFailureAddPost;
+        d[self.ksActionTestResultFailureDetails]   = self._actionTestResultFailureDetails;
+        d[self.ksActionTestResultFailureEdit]      = self._actionTestResultFailureEdit;
+        d[self.ksActionTestResultFailureEditPost]  = self._actionTestResultFailureEditPost;
 
         d[self.ksActionViewLog]                 = self.actionViewLog;
@@ -813,5 +824,5 @@
         return WuiDispatcherBase._generatePage(self)
 
-    def actionTestResultDetails(self):
+    def _actionTestResultDetails(self):
         """Show test case execution result details."""
         from testmanager.webui.wuitestresult import WuiTestResult;
@@ -849,4 +860,44 @@
         return True
 
+    def _actionTestResultFailureAdd(self):
+        """ Pro forma. """
+        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
+        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+        return self._actionGenericFormAdd(TestResultFailureData, WuiTestResultFailure);
+
+    def _actionTestResultFailureAddPost(self):
+        """Add test result failure result"""
+        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
+        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+        if self.ksParamRedirectTo not in self._dParams:
+            raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
+
+        return self._actionGenericFormAddPost(TestResultFailureData, TestResultFailureLogic,
+                                              WuiTestResultFailure, self.ksActionResultsUnGrouped);
+
+    def _actionTestResultFailureDetails(self):
+        """ Pro forma. """
+        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
+        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+        return self._actionGenericFormDetails(TestResultFailureData, TestResultFailureLogic,
+                                              WuiTestResultFailure, 'idTestResult');
+
+    def _actionTestResultFailureEdit(self):
+        """ Pro forma. """
+        from testmanager.core.testresults import TestResultFailureData;
+        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+        return self._actionGenericFormEdit(TestResultFailureData, WuiTestResultFailure,
+                                           TestResultFailureData.ksParam_idTestResult);
+
+    def _actionTestResultFailureEditPost(self):
+        """Edit test result failure result"""
+        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
+        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+        if self.ksParamRedirectTo not in self._dParams:
+            raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
+
+        return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic,
+                                               WuiTestResultFailure, self.ksActionResultsUnGrouped);
+
     def actionViewLog(self):
         """
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py	(revision 61189)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py	(revision 61217)
@@ -35,4 +35,6 @@
                                                WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml;
 from testmanager.webui.wuimain          import WuiMain;
+from testmanager.webui.wuihlpform       import WuiHlpForm;
+from testmanager.core.failurereason     import FailureReasonData, FailureReasonLogic;
 from testmanager.core.report            import ReportGraphModel;
 from testmanager.core.testbox           import TestBoxData;
@@ -40,4 +42,5 @@
 from testmanager.core.testset           import TestSetData;
 from testmanager.core.testgroup         import TestGroupData;
+from testmanager.core.testresults       import TestResultFailureData;
 from testmanager.core.build             import BuildData;
 from testmanager.core                   import db;
@@ -137,4 +140,15 @@
             sErrCnt = ' (1 error)' if oTestResult.cErrors == 1 else ' (%d errors)' % oTestResult.cErrors;
 
+        # Format bits for adding or editing the failure reason.  Level 0 is handled at the top of the page.
+        sChangeReason = '';
+        if oTestResult.cErrors > 0 and iDepth > 0:
+            dTmp = {
+                self._oDisp.ksParamAction: self._oDisp.ksActionTestResultFailureAdd if oTestResult.oReason is None else
+                                           self._oDisp.ksActionTestResultFailureEdit,
+                TestResultFailureData.ksParam_idTestResult: oTestResult.idTestResult,
+            };
+            sChangeReason = ' <a href="?%s" class="tmtbl-edit-reason" onclick="addRedirectToAnchorHref(this)">%s</a> ' \
+                          % ( webutils.encodeUrlParams(dTmp), WuiContentBase.ksShortEditLinkHtml );
+
         # Format the include in graph checkboxes.
         sLineage += ':%u' % (oTestResult.idStrName,);
@@ -148,7 +162,5 @@
 
         if    len(oTestResult.aoChildren) == 0 \
-          and len(oTestResult.aoValues)   == 0 \
-          and len(oTestResult.aoMsgs)     == 0 \
-          and len(oTestResult.aoFiles)    == 0:
+          and len(oTestResult.aoValues) + len(oTestResult.aoMsgs) + len(oTestResult.aoFiles) == 0:
             # Leaf - single row.
             tsEvent = oTestResult.tsCreated;
@@ -160,5 +172,5 @@
                      '  <td>%s</td>\n' \
                      '  <td>%s</td>\n' \
-                     '  <td colspan="2"%s>%s%s</td>\n' \
+                     '  <td colspan="2"%s>%s%s%s</td>\n' \
                      '  <td>%s</td>\n' \
                      ' </tr>\n' \
@@ -170,5 +182,5 @@
                        sDisplayName,
                        ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '',
-                       webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt),
+                       webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt), sChangeReason,
                        sResultGraph );
             iRow += 1;
@@ -189,10 +201,14 @@
             iRow += 1;
 
-            # Depth.
+            # Depth. Check if our error count is just reflecting the one of our children.
+            cErrorsBelow = 0;
             for oChild in oTestResult.aoChildren:
                 (sChildHtml, iRow, iFailure) = self._recursivelyGenerateEvents(oChild, sName, sLineage,
                                                                                iRow, iFailure, oTestSet, iDepth + 1);
                 sHtml += sChildHtml;
-
+                cErrorsBelow += oChild.cErrors;
+
+            if cErrorsBelow >= oTestResult.cErrors:
+                sChangeReason = '';
 
             # Messages.
@@ -263,6 +279,6 @@
 
                 sHtml += ' <tr class="%s tmtbl-events-file tmtbl-events-lvl%s">\n' \
-                         '  <td></td>\n' \
-                         '  <td>%s</td>\n' \
+                         '  <td>%s</td>\n' \
+                         '  <td></td>\n' \
                          '  <td></td>\n' \
                          '  <td>%s</td>\n' \
@@ -296,8 +312,86 @@
                 iRow += 1;
 
+        # Failure reason.
+        if oTestResult.oReason is not None:
+            sReasonText = '%s / %s' % (     oTestResult.oReason.oFailureReason.oCategory.sShort,
+                                            oTestResult.oReason.oFailureReason.sShort, );
+            sCommentHtml = '';
+            if oTestResult.oReason.sComment is not None and len(oTestResult.oReason.sComment.strip()) > 0:
+                sCommentHtml = '<br>' + webutils.escapeElem(oTestResult.oReason.sComment.strip());
+                sCommentHtml = sCommentHtml.replace('\n', '<br>');
+
+            sDetailedReason = ' <a href="?%s" class="tmtbl-show-reason">%s</a>' \
+                            % ( webutils.encodeUrlParams({ self._oDisp.ksParamAction:
+                                                           self._oDisp.ksActionTestResultFailureDetails,
+                                                           TestResultFailureData.ksParam_idTestResult:
+                                                           oTestResult.idTestResult,}),
+                                WuiContentBase.ksShortDetailsLinkHtml,);
+
+
+            sHtml += ' <tr class="%s tmtbl-events-reason tmtbl-events-lvl%s">\n' \
+                     '  <td>%s</td>\n' \
+                     '  <td colspan="2">%s</td>\n' \
+                     '  <td colspan="3">%s%s%s%s</td>\n' \
+                     '  <td>%s</td>\n' \
+                     ' </tr>\n' \
+                   % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+                        webutils.escapeElem(self.formatTsShort(oTestResult.oReason.tsEffective)),
+                        oTestResult.oReason.oAuthor.sUsername,
+                        webutils.escapeElem(sReasonText), sDetailedReason, sChangeReason,
+                        sCommentHtml,
+                       'todo');
+            iRow += 1;
+
         if oTestResult.isFailure():
             iFailure += 1;
 
         return (sHtml, iRow, iFailure);
+
+
+    def _generateMainReason(self, oTestResultTree, oTestSet):
+        """
+        Generates the form for displaying and updating the main failure reason.
+
+        oTestResultTree is an instance TestResultDataEx.
+        oTestSet is an instance of TestSetData.
+
+        """
+        _ = oTestSet;
+        sHtml = ' ';
+
+        if oTestResultTree.isFailure() or oTestResultTree.cErrors > 0:
+            sHtml += '   <h2>Failure Reason:</h2>\n';
+            oData = oTestResultTree.oReason;
+
+            # We need the failure reasons for the combobox.
+            aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why');
+            assert len(aoFailureReasons) > 0;
+
+            # For now we'll use the standard form helper.
+            sFormActionUrl = '%s?%s=%s' % ( self._oDisp.ksScriptName, self._oDisp.ksParamAction,
+                                            WuiMain.ksActionTestResultFailureAddPost if oData is None else
+                                            WuiMain.ksActionTestResultFailureEditPost )
+            oForm = WuiHlpForm('failure-reason', sFormActionUrl,
+                               sOnSubmit = WuiHlpForm.ksOnSubmit_AddReturnToFieldWithCurrentUrl);
+            oForm.addTextHidden(TestResultFailureData.ksParam_idTestResult, oTestResultTree.idTestResult);
+            oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason if oData is not None else -1,
+                              'Reason', aoFailureReasons);
+            oForm.addMultilineText(TestResultFailureData.ksParam_sComment,
+                                   oData.sComment if oData is not None else '', 'Comment');
+            if oData is not None:
+                oForm.addNonText('%s (%s)' % (oData.oAuthor.sUsername, oData.oAuthor.sUsername), 'Sheriff');
+                oForm.addNonText(oData.tsEffective, 'When');
+                oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, oData.tsEffective);
+                oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, oData.tsExpire);
+                oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor);
+            else:
+                oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, '');
+                oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, '');
+                oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, '');
+
+            oForm.addSubmit('Change Reason', );
+            sHtml += oForm.finalize();
+        return sHtml;
+
 
     def showTestCaseResultDetails(self,             # pylint: disable=R0914,R0915
@@ -534,4 +628,6 @@
 
         sHtml += '  <td valign="top" width="80%" style="padding-left:6px">\n';
+        sHtml += self._generateMainReason(oTestResultTree, oTestSet);
+
         sHtml += '   <h2>Events:</h2>\n';
         sHtml += '   <form action="#" method="get" id="graph-form">\n' \
@@ -614,15 +710,17 @@
             'Start',
             'Product Build',
-            'Validation Kit',
-            'TestBox OS',
-            'TestBox Name',
+            'Kit',
+            'Box',
+            'OS.Arch',
             'Test Case',
             'Elapsed',
             'Result',
+            'Reason',
         ];
         self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"',
                                  'align="center"', 'align="center"', 'align="center"',
                                  'align="center"', 'align="center"', 'align="center"',
-                                 'align="center"', 'align="center"', 'align="center"' ]
+                                 'align="center"', 'align="center"', 'align="center"',
+                                 'align="center"', ];
 
 
@@ -650,5 +748,5 @@
         oValidationKit = None;
         if oEntry.idBuildTestSuite is not None:
-            oValidationKit = WuiTmLink('#%d - r%s' % (oEntry.idBuildTestSuite, oEntry.iRevisionTestSuite),
+            oValidationKit = WuiTmLink('r%s' % (oEntry.iRevisionTestSuite,),
                                    WuiAdmin.ksScriptName,
                                    { WuiAdmin.ksParamAction:  WuiAdmin.ksActionBuildDetails,
@@ -656,12 +754,13 @@
                                    fBracketed = False);
 
-
-        aoTestSetLinks = [ WuiTmLink(oEntry.enmStatus,
-                                     WuiMain.ksScriptName,
-                                     { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
-                                       TestSetData.ksParam_idTestSet: oEntry.idTestSet },
-                                     fBracketed = False),];
+        aoTestSetLinks = [];
+        aoTestSetLinks.append(WuiTmLink(oEntry.enmStatus,
+                                        WuiMain.ksScriptName,
+                                        { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+                                          TestSetData.ksParam_idTestSet: oEntry.idTestSet },
+                                        fBracketed = False));
         if oEntry.cErrors > 0:
-            aoTestSetLinks.append(WuiTmLink('- %d error(s)' % (oEntry.cErrors, ),
+            aoTestSetLinks.append(WuiRawHtml('-'));
+            aoTestSetLinks.append(WuiTmLink('%d error%s' % (oEntry.cErrors, '' if oEntry.cErrors == 1 else 's', ),
                                             WuiMain.ksScriptName,
                                             { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
@@ -688,7 +787,27 @@
         sTestBoxTitle += u'CPU features:\t' + u', '.join(asFeatures);
 
+        # Reason:
+        oReason = None;
+        if oEntry.oFailureReason is not None:
+            sReasonTitle  = 'Reason:  \t%s\n' % ( oEntry.oFailureReason.sShort, );
+            sReasonTitle += 'Category:\t%s\n' % ( oEntry.oFailureReason.oCategory.sShort, );
+            sReasonTitle += 'Assigned:\t%s\n' % ( self.formatTsShort(oEntry.tsFailureReasonAssigned), );
+            sReasonTitle += 'By User: \t%s\n' % ( oEntry.oFailureReasonAssigner.sUsername, );
+            if oEntry.sFailureReasonComment is not None and len(oEntry.sFailureReasonComment) > 0:
+                sReasonTitle += 'Comment: \t%s\n' % ( self.formatTsShort(oEntry.sFailureReasonComment), );
+            if oEntry.oFailureReason.iTicket is not None and oEntry.oFailureReason.iTicket > 0:
+                sReasonTitle += 'xTracker:\t#%s\n' % ( oEntry.oFailureReason.iTicket, );
+            for i, sUrl in enumerate(oEntry.oFailureReason.asUrls):
+                sUrl = sUrl.strip();
+                if len(sUrl) > 0:
+                    sReasonTitle += 'URL#%u:  \t%s\n' % ( i, sUrl, );
+            oReason = WuiTmLink(oEntry.oFailureReason.sShort, WuiAdmin.ksScriptName,
+                                { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+                                  FailureReasonData.ksParam_idFailureReason: oEntry.oFailureReason.idFailureReason },
+                                sTitle = sReasonTitle);
+
         return [
             oEntry.tsCreated,
-            [ WuiTmLink('#%d - %s %s (%s)' % (oEntry.idBuild, oEntry.sProduct, oEntry.sVersion, oEntry.sType,),
+            [ WuiTmLink('%s %s (%s)' % (oEntry.sProduct, oEntry.sVersion, oEntry.sType,),
                         WuiMain.ksScriptName, self._dRevLinkParams, sTitle = '%s' % (oEntry.sBranch,), fBracketed = False),
               WuiSvnLinkWithTooltip(oEntry.iRevision, 'vbox'), ## @todo add sRepository TestResultListingData
@@ -699,5 +818,4 @@
               ],
             oValidationKit,
-            '%s.%s' % (oEntry.sOs, oEntry.sArch),
             [ WuiTmLink(oEntry.sTestBoxName, WuiMain.ksScriptName, self._dTestBoxLinkParams, fBracketed = False,
                         sTitle = sTestBoxTitle),
@@ -706,4 +824,5 @@
                           TestBoxData.ksParam_idTestBox: oEntry.idTestBox },
                         fBracketed = False) ],
+            '%s.%s' % (oEntry.sOs, oEntry.sArch),
             [ WuiTmLink(oEntry.sTestCaseName, WuiMain.ksScriptName, self._dTestCaseLinkParams, fBracketed = False,
                         sTitle = (oEntry.sBaseCmd + ' ' + oEntry.sArgs) if oEntry.sArgs else oEntry.sBaseCmd),
@@ -713,4 +832,5 @@
                         fBracketed = False), ],
             oEntry.tsElapsed,
-            aoTestSetLinks
+            aoTestSetLinks,
+            oReason
         ];
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py	(revision 61217)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py	(revision 61217)
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# $Id$
+
+"""
+Test Manager WUI - Dummy Test Result Failure Reason Edit Dialog - just for error handling!
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2016 Oracle Corporation
+
+This file is part of VirtualBox Open Source Edition (OSE), as
+available from http://www.virtualbox.org. This file is free software;
+you can redistribute it and/or modify it under the terms of the GNU
+General Public License (GPL) as published by the Free Software
+Foundation, in version 2 as it comes in the "COPYING" file of the
+VirtualBox OSE distribution. VirtualBox OSE is distributed in the
+hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL) only, as it comes in the "COPYING.CDDL" file of the
+VirtualBox OSE distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+"""
+__version__ = "$Revision$"
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase   import WuiFormContentBase;
+from testmanager.core.testresults       import TestResultFailureData;
+from testmanager.core.failurereason     import FailureReasonLogic;
+
+
+class WuiTestResultFailure(WuiFormContentBase):
+    """
+    WUI test result failure error form generator.
+    """
+    def __init__(self, oData, sMode, oDisp):
+        if sMode == WuiFormContentBase.ksMode_Add:
+            sTitle = 'Add Test Result Failure Reason';
+        elif sMode == WuiFormContentBase.ksMode_Edit:
+            sTitle = 'Modify Test Result Failure Reason';
+        else:
+            assert sMode == WuiFormContentBase.ksMode_Show;
+            sTitle = 'Test Result Failure Reason';
+        ## submit access.
+        WuiFormContentBase.__init__(self, oData, sMode, 'TestResultFailure', oDisp, sTitle);
+
+    def _populateForm(self, oForm, oData):
+
+        aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why');
+        oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason,
+                          'Reason', aoFailureReasons)
+        oForm.addMultilineText(TestResultFailureData.ksParam_sComment,   oData.sComment,     'Comment');
+        oForm.addIntRO(      TestResultFailureData.ksParam_idTestResult, oData.idTestResult, 'Test Result ID');
+        oForm.addTimestampRO(TestResultFailureData.ksParam_tsEffective,  oData.tsEffective,  'Effective Date');
+        oForm.addTimestampRO(TestResultFailureData.ksParam_tsExpire,     oData.tsExpire,     'Expire (excl)');
+        oForm.addIntRO(      TestResultFailureData.ksParam_uidAuthor,    oData.uidAuthor,    'Changed by UID');
+        if self._sMode != WuiFormContentBase.ksMode_Show:
+            oForm.addSubmit('Add' if self._sMode == WuiFormContentBase.ksMode_Add else 'Modify');
+        return True;
+
