Index: /trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py	(revision 61286)
@@ -109,5 +109,5 @@
     def __init__(self, oDb):
         ModelLogicBase.__init__(self, oDb)
-        self.ahCache = None;
+        self.dCache = None;
 
     def fetchForListing(self, iStart, cMaxRows, tsNow):
@@ -309,8 +309,8 @@
         Raises exception on DB error.
         """
-        if self.ahCache is None:
-            self.ahCache = self._oDb.getCache('FailureCategory');
-
-        oEntry = self.ahCache.get(idFailureCategory, None);
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('FailureCategory');
+
+        oEntry = self.dCache.get(idFailureCategory, None);
         if oEntry is None:
             self._oDb.execute('SELECT   *\n'
@@ -332,5 +332,5 @@
             if self._oDb.getRowCount() == 1:
                 oEntry = FailureCategoryData().initFromDbRow(self._oDb.fetchOne());
-                self.ahCache[idFailureCategory] = oEntry;
+                self.dCache[idFailureCategory] = oEntry;
         return oEntry;
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61286)
@@ -144,6 +144,6 @@
     def __init__(self, oDb):
         ModelLogicBase.__init__(self, oDb)
-        self.ahCache = None;
-        self.ahCacheNameAndCat = None;
+        self.dCache = None;
+        self.dCacheNameAndCat = None;
         self.oCategoryLogic = None;
         self.oUserAccountLogic = None;
@@ -404,13 +404,13 @@
     def cachedLookup(self, idFailureReason):
         """
-        Looks up the most recent FailureReasonDataEx object for uid idFailureReason
-        an object cache.
+        Looks up the most recent FailureReasonDataEx object for idFailureReason
+        via 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 self.dCache is None:
+            self.dCache = self._oDb.getCache('FailureReasonDataEx');
+        oEntry = self.dCache.get(idFailureReason, None);
         if oEntry is None:
             self._oDb.execute('SELECT   *\n'
@@ -434,5 +434,5 @@
                 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
                                                                self.oUserAccountLogic);
-                self.ahCache[idFailureReason] = oEntry;
+                self.dCache[idFailureReason] = oEntry;
         return oEntry;
 
@@ -447,8 +447,8 @@
         Raises exception on DB error.
         """
-        if self.ahCacheNameAndCat is None:
-            self.ahCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
+        if self.dCacheNameAndCat is None:
+            self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
         sKey = '%s:::%s' % (sName, sCategory,);
-        oEntry = self.ahCacheNameAndCat.get(sKey, None);
+        oEntry = self.dCacheNameAndCat.get(sKey, None);
         if oEntry is None:
             self._oDb.execute('SELECT   *\n'
@@ -482,8 +482,8 @@
                 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
                                                                self.oUserAccountLogic);
-                self.ahCacheNameAndCat[sKey] = oEntry;
+                self.dCacheNameAndCat[sKey] = oEntry;
                 if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort:
                     sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,);
-                    self.ahCacheNameAndCat[sKey2] = oEntry;
+                    self.dCacheNameAndCat[sKey2] = oEntry;
         return oEntry;
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/report.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/report.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/report.py	(revision 61286)
@@ -35,6 +35,7 @@
 from testmanager.core.dbobjcache    import DatabaseObjCache;
 from testmanager.core.failurereason import FailureReasonLogic;
-from testmanager.core.testbox       import TestBoxData;
+from testmanager.core.testbox       import TestBoxLogic, TestBoxData;
 from testmanager.core.testcase      import TestCaseLogic;
+from testmanager.core.testcaseargs  import TestCaseArgsLogic;
 from common                         import constants;
 
@@ -198,5 +199,6 @@
 class ReportTransientBase(object):
     """ Details on the test where a problem was first/last seen.  """
-    def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, iPeriod, fEnter):
+    def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, # pylint: disable=too-many-arguments
+                 iPeriod, fEnter, idSubject, oSubject):
         self.idBuild            = idBuild;      # Build ID.
         self.iRevision          = iRevision;    # SVN revision for build.
@@ -207,4 +209,6 @@
         self.iPeriod            = iPeriod;      # Data set period.
         self.fEnter             = fEnter;       # True if enter event, False if leave event.
+        self.idSubject          = idSubject;
+        self.oSubject           = oSubject;
 
 class ReportFailureReasonTransient(ReportTransientBase):
@@ -212,14 +216,7 @@
     def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone,  # pylint: disable=R0913
                  iPeriod, fEnter, oReason):
-        ReportTransientBase.__init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, iPeriod, fEnter);
+        ReportTransientBase.__init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, iPeriod, fEnter,
+                                     oReason.idFailureReason, oReason);
         self.oReason            = oReason;      # FailureReasonDataEx
-
-class ReportTestCaseFailureTransient(ReportTransientBase):
-    """ Details on the test where a test case was first/last seen.  """
-    def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone,  # pylint: disable=R0913
-                 iPeriod, fEnter, oTestCase):
-        ReportTransientBase.__init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, iPeriod, fEnter);
-        self.oTestCase          = oTestCase;      # TestCaseDataEx
-
 
 
@@ -246,9 +243,4 @@
         self.idFailureReason    = aoRow[0];
         self.oReason            = oReason;      # FailureReasonDataEx
-
-class ReportTestCaseFailureRow(ReportHitRowWithTotalBase):
-    """ The account of one test case for a period. """
-    def __init__(self, aoRow, oTestCase):
-        ReportHitRowWithTotalBase.__init__(self, aoRow[0], oTestCase, aoRow[1], aoRow[4], aoRow[2], aoRow[3]);
 
 
@@ -369,9 +361,4 @@
         ReportPeriodBase.__init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo);
         self.cWithoutReason     = 0;            # Number of failed test sets without any assigned reason.
-
-class ReportTestCaseFailurePeriod(ReportPeriodWithTotalBase):
-    """ A period in ReportTestCaseFailureSet. """
-    def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo):
-        ReportPeriodWithTotalBase.__init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo);
 
 
@@ -513,8 +500,4 @@
         ReportPeriodSetBase.__init__(self, 'idFailureReason');
 
-class ReportTestCaseFailureSet(ReportPeriodSetWithTotalBase):
-    """ What ReportLazyModel.getTestCaseFailures returns. """
-    def __init__(self):
-        ReportPeriodSetWithTotalBase.__init__(self, 'idTestCase');
 
 
@@ -720,14 +703,44 @@
         Gets the test case failures of the subject in the specified period.
 
-        Returns a ReportTestCaseFailureSet instance.
-
-        """
-
-        oTestCaseLogic = TestCaseLogic(self._oDb);
+        Returns a ReportPeriodSetWithTotalBase instance.
+
+        """
+        return self._getSimpleFailures('idTestCase', TestCaseLogic);
+
+
+    def getTestCaseVariationFailures(self):
+        """
+        Gets the test case failures of the subject in the specified period.
+
+        Returns a ReportPeriodSetWithTotalBase instance.
+
+        """
+        return self._getSimpleFailures('idTestCaseArgs', TestCaseArgsLogic);
+
+
+    def getTestBoxFailures(self):
+        """
+        Gets the test box failures of the subject in the specified period.
+
+        Returns a ReportPeriodSetWithTotalBase instance.
+
+        """
+        return self._getSimpleFailures('idTestBox', TestBoxLogic);
+
+
+    def _getSimpleFailures(self, sIdColumn, oCacheLogicType, sIdAttr = None):
+        """
+        Gets the test box failures of the subject in the specified period.
+
+        Returns a ReportPeriodSetWithTotalBase instance.
+
+        """
+
+        oLogic = oCacheLogicType(self._oDb);
+        oSet = ReportPeriodSetWithTotalBase(sIdColumn if sIdAttr is None else sIdAttr);
 
         # Retrieve the period results.
-        oSet = ReportTestCaseFailureSet();
         for iPeriod in xrange(self.cPeriods):
-            self._oDb.execute('SELECT   idTestCase,\n'
+            self._oDb.execute('SELECT   ' + sIdColumn + ',\n'
                               '         COUNT(CASE WHEN enmStatus >= \'failure\' THEN 1 END),\n'
                               '         MIN(tsDone),\n'
@@ -738,14 +751,14 @@
                               + self.getExtraWhereExprForPeriod(iPeriod)
                               + self.getExtraSubjectWhereExpr() + '\n'
-                              'GROUP BY idTestCase\n');
+                              'GROUP BY ' + sIdColumn + '\n');
             aaoRows = self._oDb.fetchAll()
 
-            oPeriod = ReportTestCaseFailurePeriod(oSet, iPeriod, self.getStraightPeriodDesc(iPeriod),
-                                                  self.getPeriodStart(iPeriod), self.getPeriodEnd(iPeriod));
+            oPeriod = ReportPeriodWithTotalBase(oSet, iPeriod, self.getStraightPeriodDesc(iPeriod),
+                                                self.getPeriodStart(iPeriod), self.getPeriodEnd(iPeriod));
 
             for aoRow in aaoRows:
-                oTestCase = oTestCaseLogic.cachedLookup(aoRow[0]);
-                oPeriodRow = ReportTestCaseFailureRow(aoRow, oTestCase);
-                oPeriod.appendRow(oPeriodRow, oTestCase.idTestCase, oTestCase);
+                oSubject = oLogic.cachedLookup(aoRow[0]);
+                oPeriodRow = ReportHitRowWithTotalBase(aoRow[0], oSubject, aoRow[1], aoRow[4], aoRow[2], aoRow[3]);
+                oPeriod.appendRow(oPeriodRow, aoRow[0], oSubject);
 
             oSet.appendPeriod(oPeriod);
@@ -762,12 +775,14 @@
         for iPeriod in xrange(1, self.cPeriods):
             oPeriod = oSet.aoPeriods[iPeriod];
-            for oTestCase in oPeriod.dFirst.values():
-                oSet.aoEnterInfo.append(self._getEdgeTestCaseFailureOccurence(oTestCase, iPeriod, fEnter = True));
+            for idSubject, oSubject in oPeriod.dFirst.items():
+                oSet.aoEnterInfo.append(self._getEdgeSimpleFailureOccurence(idSubject, sIdColumn, oSubject,
+                                                                            iPeriod, fEnter = True));
 
         # Ditto for reasons leaving before the last.
         for iPeriod in xrange(self.cPeriods - 1):
             oPeriod = oSet.aoPeriods[iPeriod];
-            for oTestCase in oPeriod.dLast.values():
-                oSet.aoLeaveInfo.append(self._getEdgeTestCaseFailureOccurence(oTestCase, iPeriod, fEnter = False));
+            for idSubject, oSubject in oPeriod.dLast.items():
+                oSet.aoLeaveInfo.append(self._getEdgeSimpleFailureOccurence(idSubject, sIdColumn, oSubject,
+                                                                            iPeriod, fEnter = False));
 
         oSet.finalizePass2();
@@ -775,6 +790,5 @@
         return oSet;
 
-
-    def _getEdgeTestCaseFailureOccurence(self, oTestCase, iPeriod, fEnter = True):
+    def _getEdgeSimpleFailureOccurence(self, idSubject, sIdColumn, oSubject, iPeriod, fEnter = True):
         """
         Helper for the failure reason report that finds the oldest or newest build
@@ -784,5 +798,5 @@
         is is returned.
 
-        Returns ReportFailureReasonTransient instant.
+        Returns ReportTransientBase instant.
 
         """
@@ -797,5 +811,5 @@
                           '         Builds,\n'
                           '         BuildCategories' + self.getExtraSubjectTables() + '\n'
-                          'WHERE    TestSets.idTestCase       = %s\n'
+                          'WHERE    TestSets.' + sIdColumn + '      = %s\n'
                           '     AND TestSets.idBuild          = Builds.idBuild\n'
                           '     AND TestSets.enmStatus       >= \'failure\'\n'
@@ -808,12 +822,13 @@
                           '         TestSets.tsCreated ' + sSorting + '\n'
                           'LIMIT 1\n'
-                          , ( oTestCase.idTestCase, ));
+                          , ( idSubject, ));
         aoRow = self._oDb.fetchOne();
         if aoRow is None:
-            return ReportTestCaseFailureTransient(-1, -1, 'internal-error', -1, -1,
-                                                  self._oDb.getCurrentTimestamp(), oTestCase, iPeriod, fEnter);
-        return ReportTestCaseFailureTransient(idBuild = aoRow[3], iRevision = aoRow[4], sRepository = aoRow[5],
-                                              idTestSet = aoRow[1], idTestResult = aoRow[0], tsDone = aoRow[2],
-                                              oTestCase = oTestCase, iPeriod = iPeriod, fEnter = fEnter);
+            return ReportTransientBase(-1, -1, 'internal-error', -1, -1, self._oDb.getCurrentTimestamp(),
+                                       iPeriod, fEnter, idSubject, oSubject);
+        return ReportTransientBase(idBuild = aoRow[3], iRevision = aoRow[4], sRepository = aoRow[5],
+                                   idTestSet = aoRow[1], idTestResult = aoRow[0], tsDone = aoRow[2],
+                                   iPeriod = iPeriod, fEnter = fEnter, idSubject = idSubject, oSubject = oSubject);
+
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testbox.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testbox.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testbox.py	(revision 61286)
@@ -279,4 +279,5 @@
     def __init__(self, oDb):
         ModelLogicBase.__init__(self, oDb);
+        self.dCache = None;
 
     def tryFetchTestBoxByUuid(self, sTestBoxUuid):
@@ -888,4 +889,40 @@
 
 
+    def cachedLookup(self, idTestBox):
+        """
+        Looks up the most recent TestBoxData object for idTestBox via
+        an object cache.
+
+        Returns a shared TestBoxData object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('TestBoxData');
+        oEntry = self.dCache.get(idTestBox, None);
+        if oEntry is None:
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     TestBoxes\n'
+                              'WHERE    idTestBox  = %s\n'
+                              '     AND tsExpire   = \'infinity\'::TIMESTAMP\n'
+                              , (idTestBox, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     TestBoxes\n'
+                                  'WHERE    idTestBox = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idTestBox, ));
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestBox));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = TestBoxData().initFromDbRow(aaoRow);
+                self.dCache[idTestBox] = oEntry;
+        return oEntry;
+
+
+
     #
     # The virtual test sheriff interface.
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testcase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testcase.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testcase.py	(revision 61286)
@@ -941,5 +941,5 @@
     def __init__(self, oDb):
         ModelLogicBase.__init__(self, oDb)
-        self.ahCache = None;
+        self.dCache = None;
 
     def getAll(self):
@@ -1359,16 +1359,15 @@
     def cachedLookup(self, idTestCase):
         """
-        Looks up the most recent TestCaseDataEx object for uid idTestCase
-        an object cache.
+        Looks up the most recent TestCaseDataEx object for idTestCase
+        via an object cache.
 
         Returns a shared TestCaseDataEx object.  None if not found.
         Raises exception on DB error.
         """
-        if self.ahCache is None:
-            self.ahCache = self._oDb.getCache('TestCaseDataEx');
-        oEntry = self.ahCache.get(idTestCase, None);
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('TestCaseDataEx');
+        oEntry = self.dCache.get(idTestCase, None);
         if oEntry is None:
-            ##fNeedTsNow = False;
-            fNeedTsNow = True;
+            fNeedTsNow = False;
             self._oDb.execute('SELECT   *\n'
                               'FROM     TestCases\n'
@@ -1393,5 +1392,5 @@
                 tsNow  = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None;
                 oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow);
-                self.ahCache[idTestCase] = oEntry;
+                self.dCache[idTestCase] = oEntry;
         return oEntry;
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testcaseargs.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testcaseargs.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testcaseargs.py	(revision 61286)
@@ -193,4 +193,19 @@
         raise TMExceptionBase('Do not call me: %s' % (aoRow,))
 
+    def initFromDbRowEx(self, aoRow, oDb, tsConfigEff = None, tsRsrcEff = None):
+        """
+        Extended version of initFromDbRow that fills in the rest from the database.
+        """
+        TestCaseArgsData.initFromDbRow(self, aoRow);
+
+        if tsConfigEff is None: tsConfigEff = oDb.getCurrentTimestamp();
+        if tsRsrcEff is None:   tsRsrcEff   = oDb.getCurrentTimestamp();
+
+        self.oTestCase         = TestCaseData().initFromDbWithId(oDb, self.idTestCase, tsConfigEff);
+        self.aoTestCasePreReqs = TestCaseDependencyLogic(oDb).getTestCaseDeps(self.idTestCase, tsConfigEff);
+        self.aoGlobalRsrc      = TestCaseGlobalRsrcDepLogic(oDb).getTestCaseDeps(self.idTestCase, tsRsrcEff);
+
+        return self;
+
     def initFromDbWithId(self, oDb, idTestCaseArgs, tsNow = None, sPeriodBack = None):
         _ = oDb; _ = idTestCaseArgs; _ = tsNow; _ = sPeriodBack;
@@ -205,18 +220,7 @@
         Initialize from the database, given the ID of a row.
         """
-
         oDb.execute('SELECT *, CURRENT_TIMESTAMP FROM TestCaseArgs WHERE idGenTestCaseArgs = %s', (idGenTestCaseArgs,));
         aoRow = oDb.fetchOne();
-        TestCaseArgsData.initFromDbRow(self, aoRow);
-
-        tsNow = aoRow[TestCaseArgsData.kcDbColumns];
-        if tsConfigEff is None: tsConfigEff = tsNow;
-        if tsRsrcEff is None:   tsRsrcEff   = tsNow;
-
-        self.oTestCase         = TestCaseData().initFromDbWithId(oDb, self.idTestCase, tsConfigEff);
-        self.aoTestCasePreReqs = TestCaseDependencyLogic(oDb).getTestCaseDeps(self.idTestCase, tsConfigEff);
-        self.aoGlobalRsrc      = TestCaseGlobalRsrcDepLogic(oDb).getTestCaseDeps(self.idTestCase, tsRsrcEff);
-
-        return self;
+        return self.initFromDbRowEx(aoRow, oDb, tsConfigEff, tsRsrcEff);
 
     def convertFromParamNull(self):
@@ -259,4 +263,5 @@
     def __init__(self, oDb):
         ModelLogicBase.__init__(self, oDb);
+        self.dCache = None;
 
 
@@ -348,4 +353,42 @@
         pass
 
+    def cachedLookup(self, idTestCaseArgs):
+        """
+        Looks up the most recent TestCaseArgsDataEx object for idTestCaseArg
+        via in an object cache.
+
+        Returns a shared TestCaseArgDataEx object.  None if not found.
+        Raises exception on DB error.
+        """
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('TestCaseArgsDataEx');
+        oEntry = self.dCache.get(idTestCaseArgs, None);
+        if oEntry is None:
+            fNeedTsNow = False;
+            self._oDb.execute('SELECT   *\n'
+                              'FROM     TestCaseArgs\n'
+                              'WHERE    idTestCaseArgs = %s\n'
+                              '     AND tsExpire       = \'infinity\'::TIMESTAMP\n'
+                              , (idTestCaseArgs, ));
+            if self._oDb.getRowCount() == 0:
+                # Maybe it was deleted, try get the last entry.
+                self._oDb.execute('SELECT   *\n'
+                                  'FROM     TestCaseArgs\n'
+                                  'WHERE    idTestCaseArgs = %s\n'
+                                  'ORDER BY tsExpire DESC\n'
+                                  'LIMIT 1\n'
+                                  , (idTestCaseArgs, ));
+                fNeedTsNow = True;
+            elif self._oDb.getRowCount() > 1:
+                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestCaseArgs));
+
+            if self._oDb.getRowCount() == 1:
+                aaoRow = self._oDb.fetchOne();
+                oEntry = TestCaseArgsDataEx();
+                tsNow  = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None;
+                oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow, tsNow);
+                self.dCache[idTestCaseArgs] = oEntry;
+        return oEntry;
+
 
 #
Index: /trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py	(revision 61286)
@@ -121,5 +121,5 @@
     def __init__(self, oDb):
         ModelLogicBase.__init__(self, oDb)
-        self.ahCache = None;
+        self.dCache = None;
 
     def fetchForListing(self, iStart, cMaxRows, tsNow):
@@ -232,8 +232,8 @@
         Raises exception on DB error.
         """
-        if self.ahCache is None:
-            self.ahCache = self._oDb.getCache('UserAccount');
-
-        oUser = self.ahCache.get(uid, None);
+        if self.dCache is None:
+            self.dCache = self._oDb.getCache('UserAccount');
+
+        oUser = self.dCache.get(uid, None);
         if oUser is None:
             self._oDb.execute('SELECT   *\n'
@@ -255,5 +255,5 @@
             if self._oDb.getRowCount() == 1:
                 oUser = UserAccountData().initFromDbRow(self._oDb.fetchOne());
-                self.ahCache[uid] = oUser;
+                self.dCache[uid] = oUser;
         return oUser;
 
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py	(revision 61286)
@@ -35,6 +35,6 @@
 # Validation Kit imports.
 from common                             import utils;
-from testmanager.webui.wuicontentbase   import WuiListContentWithActionBase, WuiFormContentBase, WuiLinkBase, WuiSvnLink, \
-                                               WuiTmLink, WuiSpanText, WuiRawHtml;
+from testmanager.webui.wuicontentbase   import WuiContentBase, WuiListContentWithActionBase, WuiFormContentBase, WuiLinkBase, \
+                                               WuiSvnLink, WuiTmLink, WuiSpanText, WuiRawHtml;
 from testmanager.core.db                import TMDatabaseConnection;
 from testmanager.core.schedgroup        import SchedGroupLogic, SchedGroupData;
@@ -42,4 +42,21 @@
 from testmanager.core.testset           import TestSetData;
 from testmanager.core.db                import isDbTimestampInfinity;
+
+
+
+class WuiTestBoxDetailsLink(WuiTmLink):
+    """  Test box details link by ID. """
+
+    def __init__(self, idTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None):
+        from testmanager.webui.wuiadmin import WuiAdmin;
+        dParams = {
+            WuiAdmin.ksParamAction:             WuiAdmin.ksActionTestBoxDetails,
+            TestBoxData.ksParam_idTestBox:      idTestBox,
+        };
+        if tsNow is not None:
+            dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ??
+        WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed);
+        self.idTestBox = idTestBox;
+
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61286)
@@ -81,4 +81,5 @@
     ksActionReportRate                  = 'ReportRate';
     ksActionReportTestCaseFailures      = 'ReportTestCaseFailures';
+    ksActionReportTestBoxFailures       = 'ReportTestBoxFailures';
     ksActionReportFailureReasons        = 'ReportFailureReasons';
     ksActionGraphWiz                    = 'GraphWiz';
@@ -272,8 +273,14 @@
         dCurParams = oSrvGlue.getParameters()
         if dCurParams is not None:
-            asActionUrlExtras = [ self.ksParamItemsPerPage, self.ksParamEffectiveDate, self.ksParamEffectivePeriod, ];
-            for sExtraParam in asActionUrlExtras:
+            for sExtraParam in [ self.ksParamItemsPerPage, self.ksParamEffectiveDate, self.ksParamEffectivePeriod, ]:
                 if sExtraParam in dCurParams:
-                    sExtraTimeNav += '&%s' % webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]})
+                    sExtraTimeNav += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),)
+
+        # Additional URL parameters for reports
+        sExtraReports = '';
+        if dCurParams is not None:
+            for sExtraParam in [ self.ksParamReportPeriods, self.ksParamReportPeriodInHours, self.ksParamEffectiveDate, ]:
+                if sExtraParam in dCurParams:
+                    sExtraReports += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),)
 
         # Shorthand to keep within margins.
@@ -298,8 +305,9 @@
                 'Reports',          sActUrlBase + self.ksActionReportSummary,
                 [
-                    [ 'Summary',                  sActUrlBase + self.ksActionReportSummary ],
-                    [ 'Success Rate',             sActUrlBase + self.ksActionReportRate ],
-                    [ 'Test Case Failures',       sActUrlBase + self.ksActionReportTestCaseFailures ],
-                    [ 'Failure Reasons',          sActUrlBase + self.ksActionReportFailureReasons ],
+                    [ 'Summary',                  sActUrlBase + self.ksActionReportSummary                 + sExtraReports ],
+                    [ 'Success Rate',             sActUrlBase + self.ksActionReportRate                    + sExtraReports ],
+                    [ 'Test Case Failures',       sActUrlBase + self.ksActionReportTestCaseFailures        + sExtraReports ],
+                    [ 'TestBox Failures',         sActUrlBase + self.ksActionReportTestBoxFailures         + sExtraReports ],
+                    [ 'Failure Reasons',          sActUrlBase + self.ksActionReportFailureReasons          + sExtraReports ],
                 ]
             ],
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuireport.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuireport.py	(revision 61285)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuireport.py	(revision 61286)
@@ -36,4 +36,5 @@
 from testmanager.webui.wuitestresult    import WuiTestSetLink;
 from testmanager.webui.wuiadmintestcase import WuiTestCaseDetailsLink;
+from testmanager.webui.wuiadmintestbox  import WuiTestBoxDetailsLink;
 from testmanager.core.report            import ReportModelBase;
 
@@ -43,5 +44,5 @@
 
     def __init__(self, sSubject, aIdSubjects, sName = WuiContentBase.ksShortReportLink,
-                 tsNow = None, cPeriods = None, cHoursPerPeriod = None, fBracketed = False):
+                 tsNow = None, cPeriods = None, cHoursPerPeriod = None, fBracketed = False, dExtraParams = None):
         from testmanager.webui.wuimain import WuiMain;
         dParams = {
@@ -50,4 +51,6 @@
             WuiMain.ksParamReportSubjectIds:      aIdSubjects,
         };
+        if dExtraParams is not None:
+            dParams.update(dExtraParams);
         if tsNow is not None:
             dParams[WuiMain.ksParamEffectiveDate] = tsNow;
@@ -70,4 +73,15 @@
         self._fSubReport    = fSubReport;
         self._sTitle        = None;
+
+        # Additional URL parameters for reports
+        self._dExtraParams  = {};
+        dCurParams = None if oDisp is None else oDisp.getParameters();
+        if dCurParams is not None:
+            from testmanager.webui.wuimain import WuiMain;
+            for sExtraParam in [ WuiMain.ksParamReportPeriods, WuiMain.ksParamReportPeriodInHours,
+                                 WuiMain.ksParamEffectiveDate, ]:
+                if sExtraParam in dCurParams:
+                    self._dExtraParams[sExtraParam] = dCurParams[sExtraParam];
+
 
     def generateNavigator(self, sWhere):
@@ -287,114 +301,14 @@
 
 
-
-
-class WuiReportFailureReasons(WuiReportFailuresBase):
-    """
-    Generates a report displaying the failure reasons over time.
-    """
-
-    def _formatEdgeOccurenceSubject(self, oTransient):
-        return u'%s / %s' % ( webutils.escapeElem(oTransient.oReason.oCategory.sShort),
-                              webutils.escapeElem(oTransient.oReason.sShort),);
-
-    def _formatSeriesNameForTable(self, oSet, idKey):
-        oReason = oSet.dSubjects[idKey];
-        sHtml  = '<td>'
-        sHtml += u'%s / %s' % ( webutils.escapeElem(oReason.oCategory.sShort), webutils.escapeElem(oReason.sShort),);
-        sHtml += '</td>'
-        return sHtml;
-
-
-    def generateReportBody(self):
-        self._sTitle = 'Failure reasons';
-
-        #
-        # Get the data and sort the data series in descending order of badness.
-        #
-        oSet = self._oModel.getFailureReasons();
-        aidSortedRaw = sorted(oSet.dSubjects, key = lambda idReason: oSet.dcHitsPerId[idReason], reverse = True);
-
-        #
-        # Generate table and transition list. These are the most useful ones with the current graph machinery.
-        #
-        sHtml  = self._generateTableForSet(oSet, 'Test Cases', aidSortedRaw);
-        sHtml += self._generateTransitionList(oSet);
-
-        #
-        # Check if most of the stuff is without any assign reason, if so, skip
-        # that part of the graph so it doesn't offset the interesting bits.
-        #
-        fIncludeWithoutReason = True;
-        for oPeriod in reversed(oSet.aoPeriods):
-            if oPeriod.cWithoutReason > oSet.cMaxHits * 4:
-                fIncludeWithoutReason = False;
-                sHtml += '<p>Warning: Many failures without assigned reason!</p>\n';
-                break;
-
-        #
-        # Generate the graph.
-        #
-        fGenerateGraph = len(aidSortedRaw) <= 9 and len(aidSortedRaw) > 0; ## Make this configurable.
-        if fGenerateGraph:
-            aidSorted = aidSortedRaw;
-
-            asNames = [];
-            for idReason in aidSorted:
-                oReason = oSet.dSubjects[idReason];
-                asNames.append('%s / %s' % (oReason.oCategory.sShort, oReason.sShort,) )
-            if fIncludeWithoutReason:
-                asNames.append('No reason');
-
-            oTable = WuiHlpGraphDataTable('Period', asNames);
-
-            cMax = oSet.cMaxHits;
-            for _, oPeriod in enumerate(reversed(oSet.aoPeriods)):
-                aiValues = [];
-
-                for idReason in aidSorted:
-                    oRow = oPeriod.dRowsById.get(idReason, None);
-                    iValue = oRow.cHits if oRow is not None else 0;
-                    aiValues.append(iValue);
-
-                if fIncludeWithoutReason:
-                    aiValues.append(oPeriod.cWithoutReason);
-                    if oPeriod.cWithoutReason > cMax:
-                        cMax = oPeriod.cWithoutReason;
-
-                oTable.addRow(oPeriod.sDesc, aiValues);
-
-            oGraph = WuiHlpBarGraph('failure-reason', oTable, self._oDisp);
-            oGraph.setRangeMax(max(cMax + 1, 3));
-            sHtml += oGraph.renderGraph();
-        return sHtml;
-
-
-class WuiReportTestCaseFailures(WuiReportFailuresBase):
-    """
-    Generates a report displaying the failure reasons over time.
-    """
-
-    def _formatEdgeOccurenceSubject(self, oTransient):
-        sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oTestCase.sName),);
-        sHtml += WuiTestCaseDetailsLink(oTransient.oTestCase.idTestCase, fBracketed = False).toHtml();
-        return sHtml;
-
-    def _formatSeriesNameForTable(self, oSet, idKey):
-        oTestCase = oSet.dSubjects[idKey];
-        sHtml  = '<td>'
-        sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCase.idTestCase, sName = oTestCase.sName).toHtml();
-        sHtml += u' ';
-        sHtml += WuiTestCaseDetailsLink(oTestCase.idTestCase).toHtml();
-        sHtml += '</td>'
-        return sHtml;
-
-    def generateReportBody(self):
-        self._sTitle = 'Test Case Failures';
-
-
-        #
-        # Get the data and sort the data series in descending order of badness.
-        #
-        oSet = self._oModel.getTestCaseFailures();
+class WuiReportFailuresWithTotalBase(WuiReportFailuresBase):
+    """
+    For ReportPeriodSetWithTotalBase.
+    """
+
+    def _getSortedIds(self, oSet):
+        """
+        Get default sorted subject IDs.
+        """
+
         if self._oModel.tsNow is not None and False:
             # Sort the total.
@@ -410,14 +324,11 @@
                 else:               dTmp[idKey] = oRow.cHits * 10000 / max(1, oRow.cTotal);
             aidSortedRaw = sorted(dTmp, key = lambda idKey: dTmp[idKey], reverse = True);
-
-        #
-        # Generate table and transition list. These are the most useful ones with the current graph machinery.
-        #
-        sHtml  = self._generateTableForSet(oSet, 'Test Cases', aidSortedRaw);
-        sHtml += self._generateTransitionList(oSet);
-
-        #
-        # Generate the graph.
-        #
+        return aidSortedRaw;
+
+    def _generateGraph(self, oSet, sIdBase, aidSortedRaw):
+        """
+        Generates graph.
+        """
+        sHtml = u'';
         fGenerateGraph = len(aidSortedRaw) <= 6 and len(aidSortedRaw) > 0; ## Make this configurable.
         if fGenerateGraph:
@@ -459,9 +370,192 @@
                     oTable.addRow('Totals', aiValues, asValues);
 
-                oGraph = WuiHlpBarGraph('testcase-failures', oTable, self._oDisp);
+                oGraph = WuiHlpBarGraph(sIdBase, oTable, self._oDisp);
                 oGraph.setRangeMax(uPctMax);
                 sHtml += '<br>\n';
                 sHtml += oGraph.renderGraph();
-
+        return sHtml;
+
+
+
+class WuiReportFailureReasons(WuiReportFailuresBase):
+    """
+    Generates a report displaying the failure reasons over time.
+    """
+
+    def _formatEdgeOccurenceSubject(self, oTransient):
+        return u'%s / %s' % ( webutils.escapeElem(oTransient.oReason.oCategory.sShort),
+                              webutils.escapeElem(oTransient.oReason.sShort),);
+
+    def _formatSeriesNameForTable(self, oSet, idKey):
+        oReason = oSet.dSubjects[idKey];
+        sHtml  = u'<td>';
+        sHtml += u'%s / %s' % ( webutils.escapeElem(oReason.oCategory.sShort), webutils.escapeElem(oReason.sShort),);
+        sHtml += u'</td>';
+        return sHtml;
+
+
+    def generateReportBody(self):
+        self._sTitle = 'Failure reasons';
+
+        #
+        # Get the data and sort the data series in descending order of badness.
+        #
+        oSet = self._oModel.getFailureReasons();
+        aidSortedRaw = sorted(oSet.dSubjects, key = lambda idReason: oSet.dcHitsPerId[idReason], reverse = True);
+
+        #
+        # Generate table and transition list. These are the most useful ones with the current graph machinery.
+        #
+        sHtml  = self._generateTableForSet(oSet, 'Test Cases', aidSortedRaw);
+        sHtml += self._generateTransitionList(oSet);
+
+        #
+        # Check if most of the stuff is without any assign reason, if so, skip
+        # that part of the graph so it doesn't offset the interesting bits.
+        #
+        fIncludeWithoutReason = True;
+        for oPeriod in reversed(oSet.aoPeriods):
+            if oPeriod.cWithoutReason > oSet.cMaxHits * 4:
+                fIncludeWithoutReason = False;
+                sHtml += '<p>Warning: Many failures without assigned reason!</p>\n';
+                break;
+
+        #
+        # Generate the graph.
+        #
+        fGenerateGraph = len(aidSortedRaw) <= 9 and len(aidSortedRaw) > 0; ## Make this configurable.
+        if fGenerateGraph:
+            aidSorted = aidSortedRaw;
+
+            asNames = [];
+            for idReason in aidSorted:
+                oReason = oSet.dSubjects[idReason];
+                asNames.append('%s / %s' % (oReason.oCategory.sShort, oReason.sShort,) )
+            if fIncludeWithoutReason:
+                asNames.append('No reason');
+
+            oTable = WuiHlpGraphDataTable('Period', asNames);
+
+            cMax = oSet.cMaxHits;
+            for _, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+                aiValues = [];
+
+                for idReason in aidSorted:
+                    oRow = oPeriod.dRowsById.get(idReason, None);
+                    iValue = oRow.cHits if oRow is not None else 0;
+                    aiValues.append(iValue);
+
+                if fIncludeWithoutReason:
+                    aiValues.append(oPeriod.cWithoutReason);
+                    if oPeriod.cWithoutReason > cMax:
+                        cMax = oPeriod.cWithoutReason;
+
+                oTable.addRow(oPeriod.sDesc, aiValues);
+
+            oGraph = WuiHlpBarGraph('failure-reason', oTable, self._oDisp);
+            oGraph.setRangeMax(max(cMax + 1, 3));
+            sHtml += oGraph.renderGraph();
+        return sHtml;
+
+
+class WuiReportTestCaseFailures(WuiReportFailuresWithTotalBase):
+    """
+    Generates a report displaying the failure reasons over time.
+    """
+
+    def _formatEdgeOccurenceSubject(self, oTransient):
+        sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),);
+        sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml();
+        return sHtml;
+
+    def _formatSeriesNameForTable(self, oSet, idKey):
+        oTestCase = oSet.dSubjects[idKey];
+        sHtml  = u'<td>';
+        sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCase.idTestCase, sName = oTestCase.sName,
+                                      dExtraParams = self._dExtraParams).toHtml();
+        sHtml += u' ';
+        sHtml += WuiTestCaseDetailsLink(oTestCase.idTestCase).toHtml();
+        sHtml += u'</td>';
+        return sHtml;
+
+    def generateReportBody(self):
+        self._sTitle = 'Test Case Failures';
+        oSet = self._oModel.getTestCaseFailures();
+        aidSortedRaw = self._getSortedIds(oSet);
+
+        sHtml  = self._generateTableForSet(oSet, 'Test Cases', aidSortedRaw);
+        sHtml += self._generateTransitionList(oSet);
+        sHtml += self._generateGraph(oSet, 'testcase-graph', aidSortedRaw);
+        return sHtml;
+
+
+class WuiReportTestCaseArgsFailures(WuiReportFailuresWithTotalBase):
+    """
+    Generates a report displaying the failure reasons over time.
+    """
+
+    @staticmethod
+    def _formatName(oTestCaseArgs):
+        if oTestCaseArgs.sSubName is not None and len(oTestCaseArgs.sSubName) > 0:
+            sName = u'%s / %s'  % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.sSubName, );
+        else:
+            sName = u'%s / #%u' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.idTestCaseArgs, );
+        return sName;
+
+    def _formatEdgeOccurenceSubject(self, oTransient):
+        sHtml  = u'%s ' % ( webutils.escapeElem(self._formatName(oTransient.oSubject)),);
+        sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml();
+        return sHtml;
+
+    def _formatSeriesNameForTable(self, oSet, idKey):
+        oTestCaseArgs = oSet.dSubjects[idKey];
+        sHtml  = u'<td>';
+        sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCaseArgs, oTestCaseArgs.idTestCaseArgs,
+                                      sName = self._formatName(oTestCaseArgs), dExtraParams = self._dExtraParams).toHtml();
+        sHtml += u' ';
+        sHtml += WuiTestCaseDetailsLink(oTestCaseArgs.idTestCase).toHtml();
+        sHtml += u'</td>';
+        return sHtml;
+
+    def generateReportBody(self):
+        self._sTitle = 'Test Case Variation Failures';
+        oSet = self._oModel.getTestCaseVariationFailures();
+        aidSortedRaw = self._getSortedIds(oSet);
+
+        sHtml  = self._generateTableForSet(oSet, 'Test Case Variations', aidSortedRaw);
+        sHtml += self._generateTransitionList(oSet);
+        sHtml += self._generateGraph(oSet, 'testcasearg-graph', aidSortedRaw);
+        return sHtml;
+
+
+
+class WuiReportTestBoxFailures(WuiReportFailuresWithTotalBase):
+    """
+    Generates a report displaying the failure reasons over time.
+    """
+
+    def _formatEdgeOccurenceSubject(self, oTransient):
+        sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),);
+        sHtml += WuiTestBoxDetailsLink(oTransient.oSubject.idTestBox, fBracketed = False).toHtml();
+        return sHtml;
+
+    def _formatSeriesNameForTable(self, oSet, idKey):
+        oTestBox = oSet.dSubjects[idKey];
+        sHtml  = u'<td>';
+        sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestBox.idTestBox, sName = oTestBox.sName,
+                                      dExtraParams = self._dExtraParams).toHtml();
+        sHtml += u' ';
+        sHtml += WuiTestBoxDetailsLink(oTestBox.idTestBox).toHtml();
+        sHtml += u'</td>';
+        return sHtml;
+
+    def generateReportBody(self):
+        self._sTitle = 'Test Box Failures';
+        oSet = self._oModel.getTestBoxFailures();
+        aidSortedRaw = self._getSortedIds(oSet);
+
+        sHtml  = self._generateTableForSet(oSet, 'Test Boxes', aidSortedRaw);
+        sHtml += self._generateTransitionList(oSet);
+        sHtml += self._generateGraph(oSet, 'testbox-graph', aidSortedRaw);
         return sHtml;
 
@@ -477,11 +571,19 @@
              % (self._oModel.sSubject, self._oModel.aidSubjects,);
 
-        oSuccessRate      = WuiReportSuccessRate(     self._oModel, self._dParams, fSubReport = True,
-                                                      fnDPrint = self._fnDPrint, oDisp = self._oDisp);
-        oTestCaseFailures = WuiReportTestCaseFailures(self._oModel, self._dParams, fSubReport = True,
-                                                      fnDPrint = self._fnDPrint, oDisp = self._oDisp);
-        oFailureReasons   = WuiReportFailureReasons(  self._oModel, self._dParams, fSubReport = True,
-                                                      fnDPrint = self._fnDPrint, oDisp = self._oDisp);
-        for oReport in [oSuccessRate, oTestCaseFailures, oFailureReasons, ]:
+        aoReports = [];
+
+        aoReports.append(WuiReportSuccessRate(     self._oModel, self._dParams, fSubReport = True,
+                                                   fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+        aoReports.append(WuiReportTestCaseFailures(self._oModel, self._dParams, fSubReport = True,
+                                                   fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+        if self._oModel.sSubject == ReportModelBase.ksSubTestCase:
+            aoReports.append(WuiReportTestCaseArgsFailures(self._oModel, self._dParams, fSubReport = True,
+                                                           fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+        aoReports.append(WuiReportTestBoxFailures( self._oModel, self._dParams, fSubReport = True,
+                                                   fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+        aoReports.append(WuiReportFailureReasons(  self._oModel, self._dParams, fSubReport = True,
+                                                   fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+
+        for oReport in aoReports:
             (sTitle, sContent) = oReport.show();
             sHtml += '<br>'; # drop this layout hack
