Index: trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 56808)
+++ trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 56809)
@@ -513,23 +513,171 @@
     #
 
-    ksResultsGroupingTypeNone       = 'ResultsGroupingTypeNone'
-    ksResultsGroupingTypeTestGroup  = 'ResultsGroupingTypeTestGroup'
-    ksResultsGroupingTypeBuildRev   = 'ResultsGroupingTypeBuild'
-    ksResultsGroupingTypeTestBox    = 'ResultsGroupingTypeTestBox'
-    ksResultsGroupingTypeTestCase   = 'ResultsGroupingTypeTestCase'
-    ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup'
+    ksResultsGroupingTypeNone       = 'ResultsGroupingTypeNone';
+    ksResultsGroupingTypeTestGroup  = 'ResultsGroupingTypeTestGroup';
+    ksResultsGroupingTypeBuildRev   = 'ResultsGroupingTypeBuild';
+    ksResultsGroupingTypeTestBox    = 'ResultsGroupingTypeTestBox';
+    ksResultsGroupingTypeTestCase   = 'ResultsGroupingTypeTestCase';
+    ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup';
+
+    #kdResultGroupingMapOld = {
+    #    ksResultsGroupingTypeNone:       ('TestSets',            None,                      None),
+    #    ksResultsGroupingTypeTestGroup:  ('TestSets',            'TestSets.idTestGroup',    None),
+    #    ksResultsGroupingTypeTestBox:    ('TestSets',            'TestSets.idTestBox',      None),
+    #    ksResultsGroupingTypeTestCase:   ('TestSets',            'TestSets.idTestCase',     None),
+    #    ksResultsGroupingTypeBuildRev:   ('TestSets, Builds',    'Builds.iRevision',
+    #                                      ' AND Builds.idBuild      = TestSets.idBuild'
+    #                                      ' AND Builds.tsExpire     > TestSets.tsCreated'
+    #                                      ' AND Builds.tsEffective <= TestSets.tsCreated' ),
+    #    ksResultsGroupingTypeSchedGroup: ('TestSets, TestBoxes', 'TestBoxes.idSchedGroup',
+    #                                      ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox'),
+    #};
+
+    ## @name Result sorting options.
+    ## @{
+    ksResultsSortByRunningAndStart      = 'ResultsSortByRunningAndStart'; ##< Default
+    ksResultsSortByBuildRevision        = 'ResultsSortByBuildRevision';
+    ksResultsSortByTestBoxName          = 'ResultsSortByTestBoxName';
+    ksResultsSortByTestBoxOs            = 'ResultsSortByTestBoxOs';
+    ksResultsSortByTestBoxOsVersion     = 'ResultsSortByTestBoxOsVersion';
+    ksResultsSortByTestBoxOsArch        = 'ResultsSortByTestBoxOsArch';
+    ksResultsSortByTestBoxArch          = 'ResultsSortByTestBoxArch';
+    ksResultsSortByTestBoxCpuVendor     = 'ResultsSortByTestBoxCpuVendor';
+    ksResultsSortByTestBoxCpuName       = 'ResultsSortByTestBoxCpuName';
+    ksResultsSortByTestBoxCpuRev        = 'ResultsSortByTestBoxCpuRev';
+    ksResultsSortByTestBoxCpuFeatures   = 'ResultsSortByTestBoxCpuFeatures';
+    ksResultsSortByTestCaseName         = 'ResultsSortByTestCaseName';
+    kasResultsSortBy = {
+        ksResultsSortByRunningAndStart,
+        ksResultsSortByBuildRevision,
+        ksResultsSortByTestBoxName,
+        ksResultsSortByTestBoxOs,
+        ksResultsSortByTestBoxOsVersion,
+        ksResultsSortByTestBoxOsArch,
+        ksResultsSortByTestBoxArch,
+        ksResultsSortByTestBoxCpuVendor,
+        ksResultsSortByTestBoxCpuName,
+        ksResultsSortByTestBoxCpuRev,
+        ksResultsSortByTestBoxCpuFeatures,
+        ksResultsSortByTestCaseName,
+    };
+    ## Used by the WUI for generating the drop down.
+    kaasResultsSortByTitles = (
+        ( ksResultsSortByRunningAndStart,       'Running & Start TS' ),
+        ( ksResultsSortByBuildRevision,         'Build Revision' ),
+        ( ksResultsSortByTestBoxName,           'TestBox Name' ),
+        ( ksResultsSortByTestBoxOs,             'O/S' ),
+        ( ksResultsSortByTestBoxOsVersion,      'O/S Version' ),
+        ( ksResultsSortByTestBoxOsArch,         'O/S & Architecture' ),
+        ( ksResultsSortByTestBoxArch,           'Architecture' ),
+        ( ksResultsSortByTestBoxCpuVendor,      'CPU Vendor' ),
+        ( ksResultsSortByTestBoxCpuName,        'CPU Vendor & Name' ),
+        ( ksResultsSortByTestBoxCpuRev,         'CPU Vendor & Revision' ),
+        ( ksResultsSortByTestBoxCpuFeatures,    'CPU Features' ),
+        ( ksResultsSortByTestCaseName,          'Test Case Name' ),
+    );
+    ## @}
+
+    ## Default sort by map.
+    kdResultSortByMap= {
+        ksResultsSortByRunningAndStart:  ('', None, None, ''),
+        ksResultsSortByBuildRevision: (
+            # Sorting tables.
+            ', Builds',
+            # Sorting table join(s).
+            ' AND TestSets.idBuild    = Builds.idBuild'
+            ' AND Builds.tsExpire    >= TestSets.tsCreated'
+            ' AND Builds.tsEffective <= TestSets.tsCreated',
+            # Start of ORDER BY statement.
+            ' Builds.iRevision DESC',
+            # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement.
+            ''  ),
+        ksResultsSortByTestBoxName: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sName DESC',
+            '' ),
+        ksResultsSortByTestBoxOsArch: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sOs, TestBoxes.sCpuArch',
+            ''  ),
+        ksResultsSortByTestBoxOs: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sOs',
+            ''  ),
+        ksResultsSortByTestBoxOsVersion: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sOs, TestBoxes.sOsVersion DESC',
+            ''  ),
+        ksResultsSortByTestBoxArch: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sCpuArch',
+            ''  ),
+        ksResultsSortByTestBoxCpuVendor: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sCpuVendor',
+            ''  ),
+        ksResultsSortByTestBoxCpuName: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sCpuVendor, TestBoxes.sCpuName',
+            ''  ),
+        ksResultsSortByTestBoxCpuRev: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.sCpuVendor, TestBoxes.lCpuRevision DESC',
+            ', TestBoxes.lCpuRevision'  ),
+        ksResultsSortByTestBoxCpuFeatures: (
+            ', TestBoxes',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC',
+            ', TestBoxes.cCpus' ),
+        ksResultsSortByTestCaseName: (
+            ', TestCases',
+            ' AND TestSets.idGenTestCase = TestCases.idGenTestCase',
+            ' TestCases.sName',
+            ''  ),
+    };
 
     kdResultGroupingMap = {
-        ksResultsGroupingTypeNone:       ('TestSets',            None,                      None),
-        ksResultsGroupingTypeTestGroup:  ('TestSets',            'TestSets.idTestGroup',    None),
-        ksResultsGroupingTypeTestBox:    ('TestSets',            'TestSets.idTestBox',      None),
-        ksResultsGroupingTypeTestCase:   ('TestSets',            'TestSets.idTestCase',     None),
-        ksResultsGroupingTypeBuildRev:   ('TestSets, Builds',    'Builds.iRevision',
-                                          ' AND Builds.idBuild      = TestSets.idBuild'
-                                          ' AND Builds.tsExpire     > TestSets.tsCreated'
-                                          ' AND Builds.tsEffective <= TestSets.tsCreated' ),
-        ksResultsGroupingTypeSchedGroup: ('TestSets, TestBoxes', 'TestBoxes.idSchedGroup',
-                                          ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox'),
-    }
+        ksResultsGroupingTypeNone: (
+            # Grouping tables;     # Grouping field;          # Grouping where addition.  # Sort by overrides.
+            'TestSets',            None,                      None,                       {}
+        ),
+        ksResultsGroupingTypeTestGroup:  ('TestSets',   'TestSets.idTestGroup',     None, {}),
+        ksResultsGroupingTypeTestBox:    ('TestSets',   'TestSets.idTestBox',       None, {}),
+        ksResultsGroupingTypeTestCase:   ('TestSets',   'TestSets.idTestCase',      None, {}),
+        ksResultsGroupingTypeBuildRev: (
+            'TestSets, Builds',
+            'Builds.iRevision',
+            ' AND Builds.idBuild      = TestSets.idBuild'
+            ' AND Builds.tsExpire     > TestSets.tsCreated'
+            ' AND Builds.tsEffective <= TestSets.tsCreated',
+            { ksResultsSortByBuildRevision: ( '', None,  ' Builds.iRevision DESC' ), }
+        ),
+        ksResultsGroupingTypeSchedGroup: (
+            'TestSets, TestBoxes',
+            'TestBoxes.idSchedGroup',
+            ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+            { ksResultsSortByTestBoxName:       ( '', None, ' TestBoxes.sName DESC', '' ),
+              ksResultsSortByTestBoxOsArch:     ( '', None, ' TestBoxes.sOs, TestBoxes.sCpuArch', ''  ),
+              ksResultsSortByTestBoxOs:         ( '', None,  ' TestBoxes.sOs', ''  ),
+              ksResultsSortByTestBoxOsVersion:  ( '', None, ' TestBoxes.sOs, TestBoxes.sOsVersion DESC', ''  ),
+              ksResultsSortByTestBoxArch:       ( '', None, ' TestBoxes.sCpuArch', ''  ),
+              ksResultsSortByTestBoxCpuVendor:  ( '', None, ' TestBoxes.sCpuVendor', ''  ),
+              ksResultsSortByTestBoxCpuName:    ( '', None, ' TestBoxes.sCpuVendor, TestBoxes.sCpuName', ''  ),
+              ksResultsSortByTestBoxCpuRev: (
+                  '', None,  ' TestBoxes.sCpuVendor, TestBoxes.lCpuRevision DESC', ', TestBoxes.lCpuRevision'  ),
+              ksResultsSortByTestBoxCpuFeatures: (
+                  ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, '
+                  + 'TestBoxes.cCpus DESC',
+                  ', TestBoxes.cCpus' ), }
+        ),
+    };
+
 
     def _getTimePeriodQueryPart(self, tsNow, sInterval):
@@ -555,6 +703,6 @@
         return sRet
 
-    def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
-                               fOnlyFailures):
+    def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultSortBy,
+                               enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
         """
         Fetches TestResults table content.
@@ -574,9 +722,13 @@
         # Get SQL query parameters
         #
-        if enmResultsGroupingType is None:
-            raise TMExceptionBase('Unknown grouping type')
-        if enmResultsGroupingType not in self.kdResultGroupingMap:
-            raise TMExceptionBase('Unknown grouping type')
-        sTables, sGroupingField, sGroupingCondition = self.kdResultGroupingMap[enmResultsGroupingType]
+        if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap:
+            raise TMExceptionBase('Unknown grouping type');
+        if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy:
+            raise TMExceptionBase('Unknown sorting');
+        sGroupingTables, sGroupingField, sGroupingCondition, dSortingOverrides = self.kdResultGroupingMap[enmResultsGroupingType];
+        if enmResultSortBy in dSortingOverrides:
+            sSortingTables, sSortingWhere, sSortingOrderBy, sSortingColumns = dSortingOverrides[enmResultSortBy];
+        else:
+            sSortingTables, sSortingWhere, sSortingOrderBy, sSortingColumns = self.kdResultSortByMap[enmResultSortBy];
 
         #
@@ -613,5 +765,5 @@
                   '       TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
                   '       TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
-                  '       (TestSets.tsDone IS NULL) SortRunningFirst\n' \
+                  '       (TestSets.tsDone IS NULL) SortRunningFirst' + sSortingColumns + '\n' \
                   'FROM   BuildCategories,\n' \
                   '       Builds,\n' \
@@ -629,5 +781,5 @@
                   '                 TestSets.idGenTestCase AS idGenTestCase,\n' \
                   '                 TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \
-                  '          FROM  ' + sTables + '\n' \
+                  '          FROM  ' + sGroupingTables + sSortingTables + '\n' \
                   '          WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval);
         if fOnlyFailures:
@@ -638,5 +790,10 @@
         if sGroupingCondition is not None:
             sQuery += sGroupingCondition.replace(' AND ', '            AND ');
-        sQuery += '          ORDER BY (TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
+        if sSortingWhere is not None:
+            sQuery += sSortingWhere.replace(' AND ', '            AND ');
+        sQuery += '          ORDER BY ';
+        if sSortingOrderBy is not None:
+            sQuery += sSortingOrderBy + ',\n                ';
+        sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
                   '          LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
 
@@ -653,5 +810,8 @@
                   '   AND TestSets.idGenTestCase     = TestCases.idGenTestCase\n' \
                   '   AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n' \
-                  'ORDER BY (TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n'
+                  'ORDER BY ';
+        if sSortingOrderBy is not None:
+            sQuery += sSortingOrderBy + ',\n       ';
+        sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n';
 
         #
@@ -686,5 +846,5 @@
         if enmResultsGroupingType not in self.kdResultGroupingMap:
             raise TMExceptionBase('Unknown grouping type')
-        sTables, sGroupingField, sGroupingCondition  = self.kdResultGroupingMap[enmResultsGroupingType]
+        sGroupingTables, sGroupingField, sGroupingCondition, _  = self.kdResultGroupingMap[enmResultsGroupingType];
 
         #
@@ -692,5 +852,5 @@
         #
         sQuery = 'SELECT COUNT(idTestSet)\n' \
-                 'FROM   ' + sTables + '\n' \
+                 'FROM   ' + sGroupingTables + '\n' \
                  'WHERE  ' + self._getTimePeriodQueryPart(tsNow, sInterval);
         if fOnlyFailures:
Index: trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 56808)
+++ trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 56809)
@@ -142,12 +142,16 @@
     ## @}
 
+    ## @name Test result listing parameters.
+    ## @{
+    ## If this param is specified, then show only results for this member when results grouped by some parameter.
+    ksParamGroupMemberId        = 'GroupMemberId'
+    ## Optional parameter for indicating whether to restrict the listing to failures only.
+    ksParamOnlyFailures         = 'OnlyFailures'
+    ## Result listing sorting.
+    ksParamTestResultsSortBy    = 'enmSortBy'
+    ## @}
+
     ## Effective time period. one of the first column values in kaoResultPeriods.
     ksParamEffectivePeriod      = 'sEffectivePeriod'
-
-    ## If this param is specified, then show only results for this member when results grouped by some parameter.
-    ksParamGroupMemberId        = 'GroupMemberId'
-
-    ## Optional parameter for indicating whether to restrict the listing to failures only.
-    ksParamOnlyFailures         = 'OnlyFailures'
 
     ## Test result period values.
@@ -296,4 +300,32 @@
     #
 
+    def _generateSortBySelector(self, dParams, sPreamble, sPostamble):
+        """
+        Generate HTML code for the sort by selector.
+        """
+        if self.ksParamTestResultsSortBy in dParams:
+            enmResultSortBy = dParams[self.ksParamTestResultsSortBy];
+            del dParams[self.ksParamTestResultsSortBy];
+        else:
+            enmResultSortBy = TestResultLogic.ksResultsSortByRunningAndStart;
+
+        sHtmlSortBy  = '<form name="TimeForm" method="GET"> Sort by\n';
+        sHtmlSortBy += sPreamble;
+        sHtmlSortBy += '\n  <select name="%s" onchange="window.location=' % (self.ksParamTestResultsSortBy,);
+        sHtmlSortBy += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamTestResultsSortBy)
+        sHtmlSortBy += 'this.options[this.selectedIndex].value;" title="Sorting by">\n'
+
+        fSelected = False;
+        for enmCode, sTitle in TestResultLogic.kaasResultsSortByTitles:
+            if enmCode == enmResultSortBy:
+                fSelected = True;
+            sHtmlSortBy += '    <option value="%s"%s>%s</option>\n' \
+                         % (enmCode, ' selected="selected"' if enmCode == enmResultSortBy else '', sTitle,);
+        assert fSelected;
+        sHtmlSortBy += '  </select>\n';
+        sHtmlSortBy += sPostamble;
+        sHtmlSortBy += '\n</form>\n'
+        return sHtmlSortBy;
+
     def _generateStatusSelector(self, dParams, fOnlyFailures):
         """
@@ -318,5 +350,4 @@
         if WuiDispatcherBase.ksParamPageNo in dParams:
             del dParams[WuiDispatcherBase.ksParamPageNo]
-
 
         sHtmlTimeSelector  = '<form name="TimeForm" method="GET">\n'
@@ -546,4 +577,5 @@
         # Generate the elements.
         sHtmlStatusSelector = self._generateStatusSelector(self.getParameters(), fOnlyFailures);
+        sHtmlSortBySelector = self._generateSortBySelector(self.getParameters(), '', sHtmlStatusSelector);
         sHtmlPeriodSelector = self._generateResultPeriodSelector(self.getParameters(), sCurPeriod)
         sHtmlTimeWalker     = self._generateTimeWalker(self.getParameters(), tsEffective, sCurPeriod);
@@ -564,5 +596,5 @@
                 '</tr>\n' \
                 '<tr>\n' \
-                ' <td width=30%>' + sHtmlStatusSelector + '</td>\n' \
+                ' <td width=30%>' + sHtmlSortBySelector + '</td>\n' \
                 ' <td width=40% align=center>\n' + sHtmlPager + '</td>\n' \
                 ' <td width=30% align=right>\n' + sHtmlItemsPerPageSelector + '</td>\n'\
@@ -638,12 +670,15 @@
         oListContentType is a child of WuiListContentBase.
         """
-        cItemsPerPage     = self.getIntParam(self.ksParamItemsPerPage,  iMin =  2, iMax =   9999, iDefault = 128)
-        iPage             = self.getIntParam(self.ksParamPageNo,        iMin =  0, iMax = 999999, iDefault = 0)
-        tsEffective       = self.getEffectiveDateParam()
-        iGroupMemberId    = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1)
+        cItemsPerPage     = self.getIntParam(self.ksParamItemsPerPage,  iMin =  2, iMax =   9999, iDefault = 128);
+        iPage             = self.getIntParam(self.ksParamPageNo,        iMin =  0, iMax = 999999, iDefault = 0);
+        tsEffective       = self.getEffectiveDateParam();
+        iGroupMemberId    = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1);
         fOnlyFailures     = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
+        enmResultSortBy   = self.getStringParam(self.ksParamTestResultsSortBy,
+                                                asValidValues = TestResultLogic.kasResultsSortBy,
+                                                sDefault = TestResultLogic.ksResultsSortByRunningAndStart);
 
         # Get testing results period and validate it
-        asValidValues = [x for (x, _, _) in self.kaoResultPeriods]
+        asValidValues     = [x for (x, _, _) in self.kaoResultPeriods]
         sCurPeriod        = self.getStringParam(self.ksParamEffectivePeriod, asValidValues = asValidValues,
                                                 sDefault = self.ksResultPeriodDefault)
@@ -727,8 +762,8 @@
                                                             tsNow = tsEffective,
                                                             sInterval = sCurPeriod,
+                                                            enmResultSortBy = enmResultSortBy,
                                                             enmResultsGroupingType = enmResultsGroupingType,
                                                             iResultsGroupingValue = idMember,
-                                                            fOnlyFailures = fOnlyFailures)
-
+                                                            fOnlyFailures = fOnlyFailures);
             cEntriesMax = max(cEntriesMax, cEntries)
 
