VirtualBox

Changeset 61217 in vbox for trunk


Ignore:
Timestamp:
May 26, 2016 8:04:05 PM (9 years ago)
Author:
vboxsync
Message:

testmanager: give reason to failures (quick hack, can do prettier later).

Location:
trunk/src/VBox/ValidationKit/testmanager
Files:
1 added
14 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/VBox/ValidationKit/testmanager/core/db.py

    r61185 r61217  
    3333import datetime;
    3434import os;
     35import sys;
    3536import psycopg2;
    3637import psycopg2.extensions;
    37 import sys;
    3838
    3939# Validation Kit imports.
     
    222222            oSrvGlue.registerDebugInfoCallback(self.debugInfoCallback);
    223223
     224        # Object caches (used by database logic classes).
     225        self.ddCaches = dict();
     226
    224227    def isAutoCommitting(self):
    225228        """ Work around missing autocommit attribute in older versions."""
     
    528531        oCursor = self._oConn.cursor();
    529532        return TMDatabaseCursor(self, oCursor);
     533
     534    #
     535    # Cache support.
     536    #
     537    def getCache(self, sType):
     538        """ Returns the cache dictionary for this data type. """
     539        dRet = self.ddCaches.get(sType, None);
     540        if dRet is None:
     541            dRet = dict();
     542            self.ddCaches[sType] = dRet;
     543        return dRet;
     544
    530545
    531546    #
  • trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py

    r56295 r61217  
    8787    Failure Category logic.
    8888    """
     89
     90    def __init__(self, oDb):
     91        ModelLogicBase.__init__(self, oDb)
     92        self.ahCache = None;
    8993
    9094    def fetchForListing(self, iStart, cMaxRows, tsNow):
     
    193197                          (idFailureCategory,))
    194198        for iFailureReasonId in self._oDb.fetchAll():
    195             FailureReasonLogic(self._oDb).remove(
    196                 uidAuthor, iFailureReasonId, fNeedCommit=False)
     199            FailureReasonLogic(self._oDb).remove(uidAuthor, iFailureReasonId, fNeedCommit = False)
    197200
    198201        self._oDb.execute('UPDATE FailureCategories\n'
     
    238241
    239242        return True
     243
     244    def cachedLookup(self, idFailureCategory):
     245        """
     246        Looks up the most recent FailureCategoryData object for idFailureCategory
     247        via an object cache.
     248
     249        Returns a shared FailureCategoryData object.  None if not found.
     250        Raises exception on DB error.
     251        """
     252        if self.ahCache is None:
     253            self.ahCache = self._oDb.getCache('FailureCategory');
     254
     255        oEntry = self.ahCache.get(idFailureCategory, None);
     256        if oEntry is None:
     257            self._oDb.execute('SELECT   *\n'
     258                              'FROM     FailureCategories\n'
     259                              'WHERE    idFailureCategory = %s\n'
     260                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
     261                              , (idFailureCategory, ));
     262            if self._oDb.getRowCount() == 0:
     263                # Maybe it was deleted, try get the last entry.
     264                self._oDb.execute('SELECT   *\n'
     265                                  'FROM     FailureCategories\n'
     266                                  'WHERE    idFailureCategory = %s\n'
     267                                  'ORDER BY tsExpire\n'
     268                                  'LIMIT 1\n'
     269                                  , (idFailureCategory, ));
     270            elif self._oDb.getRowCount() > 1:
     271                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureCategory));
     272
     273            if self._oDb.getRowCount() == 1:
     274                oEntry = FailureCategoryData().initFromDbRow(self._oDb.fetchOne());
     275                self.ahCache[idFailureCategory] = oEntry;
     276        return oEntry;
     277
  • trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py

    r56295 r61217  
    3131
    3232# Validation Kit imports.
    33 from testmanager.core.base          import ModelDataBase, ModelLogicBase, TMExceptionBase
     33from testmanager.core.base              import ModelDataBase, ModelLogicBase, TMExceptionBase
     34from testmanager.core.useraccount       import UserAccountLogic;
     35
    3436
    3537
     
    9294        self.asUrls            = aoRow[8]
    9395
    94         return self
     96        return self;
     97
     98
     99class FailureReasonDataEx(FailureReasonData):
     100    """
     101    Failure Reason Data, extended version that includes the category.
     102    """
     103
     104    def __init__(self):
     105        FailureReasonData.__init__(self);
     106        self.oCategory  = None;
     107        self.oAuthor    = None;
     108
     109    def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
     110        """
     111        Re-initializes the data with a row from a SELECT * FROM FailureReasons.
     112
     113        Returns self. Raises exception if the row is None or otherwise invalid.
     114        """
     115
     116        self.initFromDbRow(aoRow);
     117        self.oCategory  = oCategoryLogic.cachedLookup(self.idFailureCategory);
     118        self.oAuthor    = oUserAccountLogic.cachedLookup(self.uidAuthor);
     119
     120        return self;
    95121
    96122
     
    99125    Failure Reason logic.
    100126    """
     127
     128    def __init__(self, oDb):
     129        ModelLogicBase.__init__(self, oDb)
     130        self.ahCache = None;
     131        self.oCategoryLogic = None;
     132        self.oUserAccountLogic = None;
    101133
    102134    def fetchForListing(self, iStart, cMaxRows, tsNow):
     
    129161        return aoRows
    130162
    131     def fetchForCombo(self, tsEffective = None):
     163    def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
    132164        """
    133165        Gets the list of Failure Reasons for a combo box.
     
    136168        """
    137169        if tsEffective is None:
    138             self._oDb.execute('SELECT   idFailureReason, sShort, sFull\n'
    139                               'FROM     FailureReasons\n'
    140                               'WHERE    tsExpire = \'infinity\'::TIMESTAMP\n'
    141                               'ORDER BY sShort')
     170            self._oDb.execute('SELECT   fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
     171                              'FROM     FailureReasons fr,\n'
     172                              '         FailureCategories fc\n'
     173                              'WHERE    fr.idFailureCategory = fc.idFailureCategory\n'
     174                              '  AND    fr.tsExpire = \'infinity\'::TIMESTAMP\n'
     175                              '  AND    fc.tsExpire = \'infinity\'::TIMESTAMP\n'
     176                              'ORDER BY sComboText')
    142177        else:
    143             self._oDb.execute('SELECT   idFailureReason, sShort, sFull\n'
    144                               'FROM     FailureReasons\n'
    145                               'WHERE    tsExpire     > %s\n'
    146                               '     AND tsEffective <= %s\n'
    147                               'ORDER BY sShort'
    148                               , (tsEffective, tsEffective))
    149         return self._oDb.fetchAll()
     178            self._oDb.execute('SELECT   fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
     179                              'FROM     FailureReasons fr,\n'
     180                              '         FailureCategories fc\n'
     181                              'WHERE    fr.idFailureCategory = fc.idFailureCategory\n'
     182                              '  AND    fr.tsExpire     > %s\n'
     183                              '  AND    fr.tsEffective <= %s\n'
     184                              '  AND    fc.tsExpire     > %s\n'
     185                              '  AND    fc.tsEffective <= %s\n'
     186                              'ORDER BY sComboText'
     187                              , (tsEffective, tsEffective, tsEffective, tsEffective));
     188        aoRows = self._oDb.fetchAll();
     189        return [(-1, sFirstEntry, '')] + aoRows;
    150190
    151191    def getById(self, idFailureReason):
     
    305345
    306346        return True
     347
     348    def cachedLookup(self, idFailureReason):
     349        """
     350        Looks up the most recent FailureReasonDataEx object for uid idFailureReason
     351        an object cache.
     352
     353        Returns a shared FailureReasonData object.  None if not found.
     354        Raises exception on DB error.
     355        """
     356        if self.ahCache is None:
     357            self.ahCache = self._oDb.getCache('FailureReasonDataEx');
     358        oEntry = self.ahCache.get(idFailureReason, None);
     359        if oEntry is None:
     360            self._oDb.execute('SELECT   *\n'
     361                              'FROM     FailureReasons\n'
     362                              'WHERE    idFailureReason = %s\n'
     363                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
     364                              , (idFailureReason, ));
     365            if self._oDb.getRowCount() == 0:
     366                # Maybe it was deleted, try get the last entry.
     367                self._oDb.execute('SELECT   *\n'
     368                                  'FROM     FailureReasons\n'
     369                                  'WHERE    idFailureReason = %s\n'
     370                                  'ORDER BY tsExpire\n'
     371                                  'LIMIT 1\n'
     372                                  , (idFailureReason, ));
     373            elif self._oDb.getRowCount() > 1:
     374                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
     375
     376            if self._oDb.getRowCount() == 1:
     377                if self.oCategoryLogic is None:
     378                    from testmanager.core.failurecategory import FailureCategoryLogic;
     379                    self.oCategoryLogic = FailureCategoryLogic(self._oDb);
     380                if self.oUserAccountLogic is None:
     381                    self.oUserAccountLogic = UserAccountLogic(self._oDb);
     382                oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
     383                                                               self.oUserAccountLogic);
     384                self.ahCache[idFailureReason] = oEntry;
     385        return oEntry;
     386
  • trunk/src/VBox/ValidationKit/testmanager/core/testresults.py

    r57680 r61217  
    3838from testmanager                    import config;
    3939from testmanager.core.base          import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, TMTooManyRows;
    40 from testmanager.core.testgroup     import TestGroupData
    41 from testmanager.core.build         import BuildDataEx
    42 from testmanager.core.testbox       import TestBoxData
    43 from testmanager.core.testcase      import TestCaseData
    44 from testmanager.core.schedgroup    import SchedGroupData
     40from testmanager.core.testgroup     import TestGroupData;
     41from testmanager.core.build         import BuildDataEx;
     42from testmanager.core.failurereason import FailureReasonLogic;
     43from testmanager.core.testbox       import TestBoxData;
     44from testmanager.core.testcase      import TestCaseData;
     45from testmanager.core.schedgroup    import SchedGroupData;
    4546from testmanager.core.systemlog     import SystemLogData, SystemLogLogic;
     47from testmanager.core.useraccount   import UserAccountLogic;
    4648
    4749
     
    143145
    144146        self.aoChildren = [];   # TestResultDataEx;
    145         self.aoValues   = [];   # TestResultValue;
    146         self.aoMsgs     = [];   # TestResultMsg;
    147         self.aoFiles    = [];   # TestResultFile;
     147        self.aoValues   = [];   # TestResultValueDataEx;
     148        self.aoMsgs     = [];   # TestResultMsgDataEx;
     149        self.aoFiles    = [];   # TestResultFileDataEx;
     150        self.oReason    = None; # TestResultReasonDataEx;
    148151
    149152    def initFromDbRow(self, aoRow):
     
    163166        self.aoMsgs     = [];
    164167        self.aoFiles    = [];
     168        self.oReason    = None;
    165169
    166170        TestResultData.initFromDbRow(self, aoRow);
     
    406410            return '%s; charset=utf-8' % (self.sMime,);
    407411        return self.sMime;
     412
     413
     414class TestResultFailureData(ModelDataBase):
     415    """
     416    Test result failure reason data.
     417    """
     418
     419    ksIdAttr                    = 'idTestResult';
     420
     421    ksParam_idTestResult        = 'TestResultFailure_idTestResult';
     422    ksParam_tsEffective         = 'TestResultFailure_tsEffective';
     423    ksParam_tsExpire            = 'TestResultFailure_tsExpire';
     424    ksParam_uidAuthor           = 'TestResultFailure_uidAuthor';
     425    ksParam_idFailureReason     = 'TestResultFailure_idFailureReason';
     426    ksParam_sComment            = 'TestResultFailure_sComment';
     427
     428    kasAllowNullAttributes      = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment' ];
     429
     430    def __init__(self):
     431        ModelDataBase.__init__(self)
     432        self.idTestResult       = None;
     433        self.tsEffective        = None;
     434        self.tsExpire           = None;
     435        self.uidAuthor          = None;
     436        self.idFailureReason    = None;
     437        self.sComment           = None;
     438
     439    def initFromDbRow(self, aoRow):
     440        """
     441        Reinitialize from a SELECT * FROM TestResultFailures.
     442        Return self. Raises exception if no row.
     443        """
     444        if aoRow is None:
     445            raise TMExceptionBase('Test result file record not found.')
     446
     447        self.idTestResult       = aoRow[0];
     448        self.tsEffective        = aoRow[1];
     449        self.tsExpire           = aoRow[2];
     450        self.uidAuthor          = aoRow[3];
     451        self.idFailureReason    = aoRow[4];
     452        self.sComment           = aoRow[5];
     453        return self;
     454
     455    def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
     456        """
     457        Initialize the object from the database.
     458        """
     459        oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
     460                                                       'SELECT *\n'
     461                                                       'FROM   TestResultFailures\n'
     462                                                       'WHERE  idTestResult = %s\n'
     463                                                       , ( idTestResult,), tsNow, sPeriodBack));
     464        aoRow = oDb.fetchOne()
     465        if aoRow is None:
     466            raise TMExceptionBase('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack));
     467        return self.initFromDbRow(aoRow);
     468
     469
     470class TestResultFailureDataEx(TestResultFailureData):
     471    """
     472    Extends TestResultFailureData by resolving reasons and user.
     473    """
     474
     475    def __init__(self):
     476        TestResultFailureData.__init__(self);
     477        self.oFailureReason     = None;
     478        self.oAuthor            = None;
     479
     480    def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
     481        """
     482        Reinitialize from a query like this:
     483            SELECT   TestResultFiles.*,
     484                     StrTabFile.sValue AS sFile,
     485                     StrTabDesc.sValue AS sDescription
     486                     StrTabKind.sValue AS sKind,
     487                     StrTabMime.sValue AS sMime,
     488            FROM ...
     489
     490        Return self. Raises exception if no row.
     491        """
     492        self.initFromDbRow(aoRow);
     493        self.oFailureReason = oFailureReasonLogic.cachedLookup(self.idFailureReason);
     494        self.oAuthor        = oUserAccountLogic.cachedLookup(self.uidAuthor);
     495        return self;
    408496
    409497
     
    453541        self.iRevisionTestSuite      = None;
    454542
    455     def initFromDbRow(self, aoRow):
     543        self.oFailureReason          = None;
     544        self.oFailureReasonAssigner  = None;
     545        self.tsFailureReasonAssigned = None;
     546        self.sFailureReasonComment   = None;
     547
     548    def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
    456549        """
    457550        Reinitialize from a database query.
     
    497590        self.iRevisionTestSuite      = aoRow[29];
    498591
     592        self.oFailureReason          = None;
     593        if aoRow[30] is not None:
     594            self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[30]);
     595        self.oFailureReasonAssigner  = None;
     596        if aoRow[31] is not None:
     597            self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[31]);
     598        self.tsFailureReasonAssigned = aoRow[32];
     599        self.sFailureReasonComment   = aoRow[33];
     600
    499601        return self
    500602
     
    503605    """Hanging offence committed by test case."""
    504606    pass;
     607
    505608
    506609class TestResultLogic(ModelLogicBase): # pylint: disable=R0903
     
    534637    ksResultsSortByTestBoxCpuFeatures   = 'ResultsSortByTestBoxCpuFeatures';
    535638    ksResultsSortByTestCaseName         = 'ResultsSortByTestCaseName';
     639    ksResultsSortByFailureReason        = 'ResultsSortByFailureReason';
    536640    kasResultsSortBy = {
    537641        ksResultsSortByRunningAndStart,
     
    547651        ksResultsSortByTestBoxCpuFeatures,
    548652        ksResultsSortByTestCaseName,
     653        ksResultsSortByFailureReason,
    549654    };
    550655    ## Used by the WUI for generating the drop down.
     
    562667        ( ksResultsSortByTestBoxCpuFeatures,    'CPU Features' ),
    563668        ( ksResultsSortByTestCaseName,          'Test Case Name' ),
     669        ( ksResultsSortByFailureReason,         'Failure Reason' ),
    564670    );
    565671    ## @}
     
    629735            ' TestCases.sName',
    630736            ''  ),
     737        ksResultsSortByFailureReason: (
     738            '', '',
     739            'sSortByFailureReason ASC',
     740            ', FailureReasons.sShort AS sSortByFailureReason' ),
    631741    };
    632742
     
    668778
    669779
     780    def __init__(self, oDb):
     781        ModelLogicBase.__init__(self, oDb)
     782        self.oFailureReasonLogic = None;
     783        self.oUserAccountLogic   = None;
     784
    670785    def _getTimePeriodQueryPart(self, tsNow, sInterval):
    671786        """
     
    752867                  '       TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
    753868                  '       TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
     869                  '       TestResultFailures.idFailureReason as idFailureReason,\n' \
     870                  '       TestResultFailures.uidAuthor as uidFailureReasonAssigner,\n' \
     871                  '       TestResultFailures.tsEffective as tsFailureReasonAssigned,\n' \
     872                  '       TestResultFailures.sComment as sFailureReasonComment,\n' \
    754873                  '       (TestSets.tsDone IS NULL) SortRunningFirst' + sSortingColumns + '\n' \
    755874                  'FROM   BuildCategories,\n' \
    756875                  '       Builds,\n' \
    757876                  '       TestBoxes,\n' \
    758                   '       TestResults,\n' \
     877                  '       TestResults LEFT OUTER JOIN TestResultFailures\n' \
     878                  '            ON TestResults.idTestResult    = TestResultFailures.idTestResult\n' \
     879                  '           AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
     880        if sSortingOrderBy.find('FailureReason') >= 0:
     881            sQuery += '\n' \
     882                      '       LEFT OUTER JOIN FailureReasons\n' \
     883                      '            ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
     884                      '           AND FailureReasons.tsExpire            = \'infinity\'::TIMESTAMP';
     885        sQuery += ',\n'\
    759886                  '       TestCases,\n' \
    760887                  '       TestCaseArgs,\n' \
     
    780907            sQuery += sSortingWhere.replace(' AND ', '            AND ');
    781908        sQuery += '          ORDER BY ';
    782         if sSortingOrderBy is not None:
     909        if sSortingOrderBy is not None and sSortingOrderBy.find('FailureReason') < 0:
    783910            sQuery += sSortingOrderBy + ',\n                ';
    784911        sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
     
    807934        self._oDb.execute(sQuery);
    808935
     936        if self.oFailureReasonLogic is None:
     937            self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
     938        if self.oUserAccountLogic is None:
     939            self.oUserAccountLogic = UserAccountLogic(self._oDb);
     940
    809941        aoRows = [];
    810942        for aoRow in self._oDb.fetchAll():
    811             aoRows.append(TestResultListingData().initFromDbRow(aoRow))
     943            aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic));
    812944
    813945        return aoRows
     
    10141146            '         EXISTS ( SELECT idTestResultFile\n'
    10151147            '           FROM   TestResultFiles\n'
    1016             '           WHERE  TestResultFiles.idTestResult  = TestResults.idTestResult ) AS fHasFiles\n'
     1148            '           WHERE  TestResultFiles.idTestResult  = TestResults.idTestResult ) AS fHasFiles,\n'
     1149            '         EXISTS ( SELECT idTestResult\n'
     1150            '           FROM   TestResultFailures\n'
     1151            '           WHERE  TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n'
    10171152            'FROM     TestResults, TestResultStrTab\n'
    10181153            'WHERE    TestResults.idTestSet = %s\n'
     
    10381173            raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
    10391174                                               % (oRoot.idTestResult, oRoot.idTestResultParent));
    1040         self._fetchResultTreeNodeExtras(oRoot, aoRow[-3], aoRow[-2], aoRow[-1]);
     1175        self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
    10411176
    10421177        # The chilren (if any).
     
    10461181            aoRow = aaoRows[iRow];
    10471182            oCur = TestResultDataEx().initFromDbRow(aoRow);
    1048             self._fetchResultTreeNodeExtras(oCur, aoRow[-3], aoRow[-2], aoRow[-1]);
     1183            self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
    10491184
    10501185            # Figure out and vet the parent.
     
    10651200        return (oRoot, dLookup);
    10661201
    1067     def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles):
     1202    def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons):
    10681203        """
    10691204        fetchResultTree worker that fetches values, message and files for the
    10701205        specified node.
    10711206        """
    1072         assert(oCurNode.aoValues == []);
    1073         assert(oCurNode.aoMsgs   == []);
    1074         assert(oCurNode.aoFiles  == []);
     1207        assert(oCurNode.aoValues  == []);
     1208        assert(oCurNode.aoMsgs    == []);
     1209        assert(oCurNode.aoFiles   == []);
     1210        assert(oCurNode.oReason is None);
    10751211
    10761212        if fHasValues:
     
    11171253                oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
    11181254
     1255        if fHasReasons or True:
     1256            if self.oFailureReasonLogic is None:
     1257                self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
     1258            if self.oUserAccountLogic is None:
     1259                self.oUserAccountLogic = UserAccountLogic(self._oDb);
     1260            self._oDb.execute('SELECT   *\n'
     1261                              'FROM     TestResultFailures\n'
     1262                              'WHERE    idTestResult = %s\n'
     1263                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
     1264                              , ( oCurNode.idTestResult, ));
     1265            if self._oDb.getRowCount() > 0:
     1266                oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic,
     1267                                                                             self.oUserAccountLogic);
     1268
    11191269        return True;
    11201270
     
    11841334        """Returns a string rep of the stack."""
    11851335        sRet = '';
    1186         for i in range(len(aoStack)):
     1336        for i, _ in enumerate(aoStack):
    11871337            sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
    11881338        return sRet;
     
    12021352            aoStack.append(TestResultData().initFromDbRow(aoRow));
    12031353
    1204         for i in range(len(aoStack)):
     1354        for i, _ in enumerate(aoStack):
    12051355            assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
    12061356
     
    14731623            if sAttr in dAttribs:
    14741624                try:
    1475                     _ = long(dAttribs[sAttr]);
     1625                    _ = long(dAttribs[sAttr]);  # pylint: disable=R0204
    14761626                except:
    14771627                    return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
     
    17611911
    17621912
     1913
     1914class TestResultFailureLogic(ModelLogicBase): # pylint: disable=R0903
     1915    """
     1916    Test result failure reason logic.
     1917    """
     1918
     1919    def __init__(self, oDb):
     1920        ModelLogicBase.__init__(self, oDb)
     1921
     1922    def getById(self, idTestResult):
     1923        """Get Test result failure reason data by idTestResult"""
     1924
     1925        self._oDb.execute('SELECT   *\n'
     1926                          'FROM     TestResultFailures\n'
     1927                          'WHERE    tsExpire   = \'infinity\'::timestamp\n'
     1928                          '  AND    idTestResult = %s;', (idTestResult,))
     1929        aRows = self._oDb.fetchAll()
     1930        if len(aRows) not in (0, 1):
     1931            raise self._oDb.integrityException(
     1932                'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
     1933        try:
     1934            return TestResultFailureData().initFromDbRow(aRows[0])
     1935        except IndexError:
     1936            return None
     1937
     1938    def addEntry(self, oData, uidAuthor, fCommit = False):
     1939        """
     1940        Add a test result failure reason record.
     1941        """
     1942
     1943        # Check if it exist first (we're adding, not editing, collisions not allowed).
     1944        oOldData = self.getById(oData.idTestResult);
     1945        if oOldData is not None:
     1946            raise TMExceptionBase('TestResult %d already have a failure reason associated with it:'
     1947                                  '%s\n'
     1948                                  'Perhaps someone else beat you to it? Or did you try resubmit?'
     1949                                  % (oData.idTestResult, oOldData));
     1950
     1951        #
     1952        # Add record.
     1953        #
     1954        self._readdEntry(uidAuthor, oData);
     1955        self._oDb.maybeCommit(fCommit);
     1956        return True;
     1957
     1958    def editEntry(self, oData, uidAuthor, fCommit = False):
     1959        """
     1960        Modifies a test result failure reason.
     1961        """
     1962
     1963        #
     1964        # Validate inputs and read in the old(/current) data.
     1965        #
     1966        assert isinstance(oData, TestResultFailureData);
     1967        dErrors = oData.validateAndConvert(self._oDb);
     1968        if len(dErrors) > 0:
     1969            raise TMExceptionBase('editEntry invalid input: %s' % (dErrors,));
     1970
     1971        oOldData = self.getById(oData.idTestResult)
     1972
     1973        #
     1974        # Update the data that needs updating.
     1975        #
     1976        if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
     1977            self._historizeEntry(oData.idTestResult);
     1978            self._readdEntry(uidAuthor, oData);
     1979        self._oDb.maybeCommit(fCommit);
     1980        return True;
     1981
     1982
     1983    def removeEntry(self, uidAuthor, idTestResult, fCascade = False, fCommit = False):
     1984        """
     1985        Deletes a test result failure reason.
     1986        """
     1987        _ = fCascade; # Not applicable.
     1988
     1989        oData = self.getById(idTestResult)
     1990        (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
     1991        if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:
     1992            self._historizeEntry(idTestResult, tsCurMinusOne);
     1993            self._readdEntry(uidAuthor, oData, tsCurMinusOne);
     1994            self._historizeEntry(idTestResult);
     1995        self._oDb.execute('UPDATE   TestResultFaillures\n'
     1996                          'SET      tsExpire       = CURRENT_TIMESTAMP\n'
     1997                          'WHERE    idTestResult   = %s\n'
     1998                          '     AND tsExpire       = \'infinity\'::TIMESTAMP\n'
     1999                          , (idTestResult,));
     2000        self._oDb.maybeCommit(fCommit);
     2001        return True;
     2002
     2003    #
     2004    # Helpers.
     2005    #
     2006
     2007    def _readdEntry(self, uidAuthor, oData, tsEffective = None):
     2008        """
     2009        Re-adds the TestResultFailure entry. Used by addEntry, editEntry and removeEntry.
     2010        """
     2011        if tsEffective is None:
     2012            tsEffective = self._oDb.getCurrentTimestamp();
     2013        self._oDb.execute('INSERT INTO TestResultFailures (\n'
     2014                          '         uidAuthor,\n'
     2015                          '         tsEffective,\n'
     2016                          '         idTestResult,\n'
     2017                          '         idFailureReason,\n'
     2018                          '         sComment)\n'
     2019                          'VALUES (%s, %s, %s, %s, %s)\n'
     2020                          , ( uidAuthor,
     2021                              tsEffective,
     2022                              oData.idTestResult,
     2023                              oData.idFailureReason,
     2024                              oData.sComment,) );
     2025        return True;
     2026
     2027
     2028    def _historizeEntry(self, idTestResult, tsExpire = None):
     2029        """ Historizes the current entry. """
     2030        if tsExpire is None:
     2031            tsExpire = self._oDb.getCurrentTimestamp();
     2032        self._oDb.execute('UPDATE TestResultFailures\n'
     2033                          'SET    tsExpire   = %s\n'
     2034                          'WHERE  idTestResult = %s\n'
     2035                          '   AND tsExpire     = \'infinity\'::TIMESTAMP\n'
     2036                          , (tsExpire, idTestResult,));
     2037        return True;
     2038
     2039
     2040
    17632041#
    17642042# Unit testing.
  • trunk/src/VBox/ValidationKit/testmanager/core/useraccount.py

    r56295 r61217  
    116116class UserAccountLogic(ModelLogicBase):
    117117    """
    118     SystemLog logic.
    119     """
     118    User account logic (for the Users table).
     119    """
     120
     121    def __init__(self, oDb):
     122        ModelLogicBase.__init__(self, oDb)
     123        self.ahCache = None;
    120124
    121125    def fetchForListing(self, iStart, cMaxRows, tsNow):
     
    221225        return UserAccountData().initFromDbRow(self._oDb.fetchOne());
    222226
     227    def cachedLookup(self, uid):
     228        """
     229        Looks up the current UserAccountData object for uid via an object cache.
     230
     231        Returns a shared UserAccountData object.  None if not found.
     232        Raises exception on DB error.
     233        """
     234        if self.ahCache is None:
     235            self.ahCache = self._oDb.getCache('UserAccount');
     236
     237        oUser = self.ahCache.get(uid, None);
     238        if oUser is None:
     239            self._oDb.execute('SELECT   *\n'
     240                              'FROM     Users\n'
     241                              'WHERE    uid = %s\n'
     242                              '     AND tsExpire = \'infinity\'::TIMESTAMP\n'
     243                              , (uid, ));
     244            if self._oDb.getRowCount() == 0:
     245                # Maybe it was deleted, try get the last entry.
     246                self._oDb.execute('SELECT   *\n'
     247                                  'FROM     Users\n'
     248                                  'WHERE    uid = %s\n'
     249                                  'ORDER BY tsExpire\n'
     250                                  'LIMIT 1\n'
     251                                  , (uid, ));
     252            elif self._oDb.getRowCount() > 1:
     253                raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), uid));
     254
     255            if self._oDb.getRowCount() == 1:
     256                oUser = UserAccountData().initFromDbRow(self._oDb.fetchOne());
     257                self.ahCache[uid] = oUser;
     258        return oUser;
     259
    223260    def resolveChangeLogAuthors(self, aoEntries):
    224261        """
     
    229266        Raises exception on DB error.
    230267        """
    231         ahCache = dict();
    232268        for oEntry in aoEntries:
    233             oEntry.sAuthor = ahCache.get(oEntry.uidAuthor, None);
    234             if oEntry.sAuthor is None and oEntry.uidAuthor is not None:
    235                 try:
    236                     oUser = UserAccountData().initFromDbWithId(self._oDb, oEntry.uidAuthor, oEntry.tsEffective);
    237                 except:
    238                     pass;
    239                 else:
    240                     ahCache[oEntry.uidAuthor] = oUser.sUsername;
    241                     oEntry.sAuthor = oUser.sUsername;
     269            oUser = self.cachedLookup(oEntry.uidAuthor)
     270            if oUser is not None:
     271                oEntry.sAuthor = oUser.sUsername;
    242272        return aoEntries;
    243273
  • trunk/src/VBox/ValidationKit/testmanager/db/Makefile.kmk

    r61181 r61217  
    2828# Need proper shell on windows.
    2929DEPTH = ../../../../..
    30 include $(KBUILD_PATH)/header.kmk
     30#include $(KBUILD_PATH)/header.kmk
    3131
    3232
  • trunk/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py

    r61184 r61217  
    262262        oDb.commit();
    263263
     264        # Correct sequences.
     265        atSequences = [
     266            ( 'UserIdSeq',              'Users',                'uid' ),
     267            ( 'GlobalResourceIdSeq',    'GlobalResources',      'idGlobalRsrc' ),
     268            ( 'BuildSourceIdSeq',       'BuildSources',         'idBuildSrc' ),
     269            ( 'TestCaseIdSeq',          'TestCases',            'idTestCase' ),
     270            ( 'TestCaseGenIdSeq',       'TestCases',            'idGenTestCase' ),
     271            ( 'TestCaseArgsIdSeq',      'TestCaseArgs',         'idTestCaseArgs' ),
     272            ( 'TestCaseArgsGenIdSeq',   'TestCaseArgs',         'idGenTestCaseArgs' ),
     273            ( 'TestGroupIdSeq',         'TestGroups',           'idTestGroup' ),
     274            ( 'SchedGroupIdSeq',        'SchedGroups',          'idSchedGroup' ),
     275            ( 'TestBoxIdSeq',           'TestBoxes',            'idTestBox' ),
     276            ( 'TestBoxGenIdSeq',        'TestBoxes',            'idGenTestBox' ),
     277            ( 'FailureCategoryIdSeq',   'FailureCategories',    'idFailureCategory' ),
     278            ( 'FailureReasonIdSeq',     'FailureReasons',       'idFailureReason' ),
     279            ( 'BuildBlacklistIdSeq',    'BuildBlacklist',       'idBlacklisting' ),
     280            ( 'BuildCategoryIdSeq',     'BuildCategories',      'idBuildCategory' ),
     281            ( 'BuildIdSeq',             'Builds',               'idBuild' ),
     282            ( 'TestResultStrTabIdSeq',  'TestResultStrTab',     'idStr' ),
     283            ( 'TestResultIdSeq',        'TestResults',          'idTestResult' ),
     284            ( 'TestResultValueIdSeq',   'TestResultValues',     'idTestResultValue' ),
     285            ( 'TestResultFileId',       'TestResultFiles',      'idTestResultFile' ),
     286            ( 'TestResultMsgIdSeq',     'TestResultMsgs',       'idTestResultMsg' ),
     287            ( 'TestSetIdSeq',           'TestSets',             'idTestSet' ),
     288            ( 'SchedQueueItemIdSeq',    'SchedQueues',          'idItem' ),
     289        ];
     290        for (sSeq, sTab, sCol) in atSequences:
     291            oDb.execute('SELECT MAX(%s) FROM %s' % (sCol, sTab,));
     292            idMax = oDb.fetchOne()[0];
     293            print '%s: idMax=%s' % (sSeq, idMax);
     294            if idMax is not None:
     295                oDb.execute('SELECT setval(\'%s\', %s)' % (sSeq, idMax));
     296
    264297        # Last step.
    265298        print 'Analyzing...'
  • trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js

    r56295 r61217  
    2727
    2828
     29/*********************************************************************************************************************************
     30*   Global Variables                                                                                                             *
     31*********************************************************************************************************************************/
     32/** Same as WuiDispatcherBase.ksParamRedirectTo. */
     33var g_ksParamRedirectTo = 'RedirectTo';
     34
    2935
    3036/**
     
    161167{
    162168    return getUnscaledElementWidth(document.getElementById(sElementId));
     169}
     170
     171/**
     172 * Gets the part of the URL needed for a RedirectTo parameter.
     173 *
     174 * @returns URL string.
     175 */
     176function getCurrentBrowerUrlPartForRedirectTo()
     177{
     178    var sWhere = window.location.href;
     179    var offTmp;
     180    var offPathKeep;
     181
     182    /* Find the end of that URL 'path' component. */
     183    var offPathEnd = sWhere.indexOf('?');
     184    if (offPathEnd < 0)
     185        offPathEnd = sWhere.indexOf('#');
     186    if (offPathEnd < 0)
     187        offPathEnd = sWhere.length;
     188
     189    /* Go backwards from the end of the and find the start of the last component. */
     190    offPathKeep = sWhere.lastIndexOf("/", offPathEnd);
     191    offTmp = sWhere.lastIndexOf(":", offPathEnd);
     192    if (offPathKeep < offTmp)
     193        offPathKeep = offTmp;
     194    offTmp = sWhere.lastIndexOf("\\", offPathEnd);
     195    if (offPathKeep < offTmp)
     196        offPathKeep = offTmp;
     197
     198    return sWhere.substring(offPathKeep + 1);
    163199}
    164200
     
    266302
    267303}
     304
     305/**
     306 * Adds the RedirecTo field with the current URL to the form.
     307 *
     308 * This is a 'onsubmit' action.
     309 *
     310 * @returns Returns success indicator (true/false).
     311 * @param   oForm               The form being submitted.
     312 */
     313function addRedirectToInputFieldWithCurrentUrl(oForm)
     314{
     315    /* Constant used here is duplicated in WuiDispatcherBase.ksParamRedirectTo */
     316    return addHiddenInputFieldToForm(oForm, 'RedirectTo', getCurrentBrowerUrlPartForRedirectTo(), null);
     317}
     318
     319/**
     320 * Adds the RedirecTo parameter to the href of the given anchor.
     321 *
     322 * This is a 'onclick' action.
     323 *
     324 * @returns Returns success indicator (true/false).
     325 * @param   oAnchor         The anchor element being clicked on.
     326 */
     327function addRedirectToAnchorHref(oAnchor)
     328{
     329    var sRedirectToParam = g_ksParamRedirectTo + '=' + encodeURIComponent(getCurrentBrowerUrlPartForRedirectTo());
     330    var sHref = oAnchor.href;
     331    if (sHref.indexOf(sRedirectToParam) < 0)
     332    {
     333        var sHash;
     334        var offHash = sHref.indexOf('#');
     335        if (offHash >= 0)
     336            sHash = sHref.substring(offHash);
     337        else
     338        {
     339            sHash   = '';
     340            offHash = sHref.length;
     341        }
     342        sHref = sHref.substring(0, offHash)
     343        if (sHref.indexOf('?') >= 0)
     344            sHref += '&';
     345        else
     346            sHref += '?';
     347        sHref += sRedirectToParam;
     348        sHref += sHash;
     349        oAnchor.href = sHref;
     350    }
     351    return true;
     352}
     353
    268354
    269355
  • trunk/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py

    r56295 r61217  
    138138
    139139    ksActionFailureReasonList       = 'FailureReasonList'
     140    ksActionFailureReasonDetails    = 'FailureReasonDetails'
    140141    ksActionFailureReasonShowAdd    = 'FailureReasonShowAdd'
    141142    ksActionFailureReasonShowEdit   = 'FailureReasonShowEdit'
     
    365366                                                                WuiAdminFailureReasonList)
    366367
    367         d[self.ksActionFailureReasonShowAdd]    = lambda: self._actionGenericFormAdd(
    368                                                                 FailureReasonData,
    369                                                                 WuiAdminFailureReason)
    370 
     368        d[self.ksActionFailureReasonDetails]    = lambda: self._actionGenericFormDetails(FailureReasonData,
     369                                                                                         FailureReasonLogic,
     370                                                                                         WuiAdminFailureReason,
     371                                                                                         'idFailureReason');
    371372        d[self.ksActionFailureReasonShowEdit]   = lambda: self._actionGenericFormEditL(
    372373                                                                FailureReasonLogic,
  • trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py

    r56295 r61217  
    7474    ## The name of the effective date (timestamp) parameter.
    7575    ksParamEffectiveDate = 'EffectiveDate';
     76
     77    ## The name of the redirect-to (test manager relative url) parameter.
     78    ksParamRedirectTo    = 'RedirectTo';
    7679
    7780    ## The name of the list-action parameter (WuiListContentWithActionBase).
     
    546549        return str(oDate);
    547550
     551    def getRedirectToParameter(self, sDefault = None):
     552        """
     553        Gets the special redirect to parameter if it exists, will Return default
     554        if not, with None being a valid default.
     555
     556        Makes sure the it doesn't got offsite.
     557        Raises exception if invalid.
     558        """
     559        if sDefault is not None or self.ksParamRedirectTo in self._dParams:
     560            sValue = self.getStringParam(self.ksParamRedirectTo, sDefault = sDefault);
     561            cch = sValue.find("?");
     562            if cch < 0:
     563                cch = sValue.find("#");
     564                if cch < 0:
     565                    cch = len(sValue);
     566            for ch in (':', '/', '\\', '..'):
     567                if sValue.find(ch, 0, cch) >= 0:
     568                    raise WuiException('Invalid character (%c) in redirect-to url: %s' % (ch, sValue,));
     569        else:
     570            sValue = None;
     571        return sValue;
     572
    548573
    549574    def _checkForUnknownParameters(self):
     
    701726        return True;
    702727
    703     def _actionGenericFormAdd(self, oDataType, oFormType):
     728    def _actionGenericFormAdd(self, oDataType, oFormType, sRedirectTo = None):
    704729        """
    705730        Generic add something form display request handler.
     
    709734        """
    710735        oData = oDataType().initFromParams(oDisp = self, fStrict = False);
     736        sRedirectTo = self.getRedirectToParameter(sRedirectTo);
    711737        self._checkForUnknownParameters();
    712738
    713739        oForm = oFormType(oData, oFormType.ksMode_Add, oDisp = self);
     740        oForm.setRedirectTo(sRedirectTo);
    714741        (self._sPageTitle, self._sPageBody) = oForm.showForm();
    715742        return True
     
    757784
    758785
    759     def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName):
     786    def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName, sRedirectTo = None):
    760787        """
    761788        Generic edit something form display request handler.
     
    766793        """
    767794
     795        tsNow    = self.getEffectiveDateParam();
    768796        idObject = self.getIntParam(sIdParamName, 0, 0x7ffffffe);
     797        sRedirectTo = self.getRedirectToParameter(sRedirectTo);
    769798        self._checkForUnknownParameters();
    770         oData = oDataType().initFromDbWithId(self._oDb, idObject);
     799        oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow = tsNow);
    771800
    772801        oContent = oFormType(oData, oFormType.ksMode_Edit, oDisp = self);
     802        oContent.setRedirectTo(sRedirectTo);
    773803        (self._sPageTitle, self._sPageBody) = oContent.showForm();
    774804        return True
     
    850880        #
    851881        oData = oDataType().initFromParams(oDisp = self, fStrict = fStrict);
     882        sRedirectTo = self.getRedirectToParameter(sRedirectTo);
    852883        self._checkForUnknownParameters();
    853884        self._assertPostRequest();
     
    864895                self._oDb.rollback();
    865896                oForm = oFormType(oData, sMode, oDisp = self);
     897                oForm.setRedirectTo(sRedirectTo);
    866898                sErrorMsg = str(oXcpt) if not config.g_kfDebugDbXcpt else '\n'.join(utils.getXcptInfo(4));
    867899                (self._sPageTitle, self._sPageBody) = oForm.showForm(sErrorMsg = sErrorMsg);
     
    961993            self._sAction = self.ksActionDefault;
    962994
    963         if self._sAction not in self._dDispatch:
     995        if isinstance(self._sAction, list) or  self._sAction not in self._dDispatch:
    964996            raise WuiException('Unknown action "%s" requested' % (self._sAction,));
    965997
  • trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py

    r56807 r61217  
    190190
    191191    ## The text/symbol for a very short edit link.
    192     ksShortEditLink    = u'\u270D'
     192    ksShortEditLink        = u'\u270D'
     193    ## HTML hex entity string for ksShortDetailsLink.
     194    ksShortEditLinkHtml    = '&#x270d;'
    193195    ## The text/symbol for a very short details link.
    194     ksShortDetailsLink = u'\u2318'
     196    ksShortDetailsLink     = u'\u2318'
     197    ## HTML hex entity string for ksShortDetailsLink.
     198    ksShortDetailsLinkHtml = '&#x2318;'
    195199
    196200
     
    327331        if sSubmitAction is None and sMode != self.ksMode_Show:
    328332            self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]);
     333        self._sRedirectTo   = None;
    329334
    330335
     
    469474                       '</div>\n';
    470475        return sNavigation;
     476
     477    def setRedirectTo(self, sRedirectTo):
     478        """
     479        For setting the hidden redirect-to field.
     480        """
     481        self._sRedirectTo = sRedirectTo;
     482        return True;
    471483
    472484    def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True):
     
    521533        try:
    522534            self._populateForm(oForm, self._oData);
     535            if self._sRedirectTo is not None:
     536                oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo);
    523537        except WuiException, oXcpt:
    524538            sContent = unicode(oXcpt)
     
    558572                dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._oData.tsEffective;
    559573                dParams[getattr(self._oData, 'ksParam_' + self._oData.ksIdAttr)] = getattr(self._oData, self._oData.ksIdAttr);
    560             dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
     574            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
    561575            aoActions.append(WuiTmLink('Details', '', dParams));
    562576
     
    629643        assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
    630644
    631         for i in range(len(aoValues)):
     645        for i, _ in enumerate(aoValues):
    632646            if i < len(self._asColumnAttribs) and len(self._asColumnAttribs[i]) > 0:
    633647                sRow += u'    <td ' + self._asColumnAttribs[i] + '>';
  • trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py

    r56295 r61217  
    4848    ksItemsList = 'ksItemsList'
    4949
    50     def __init__(self, sId, sAction, dErrors = None, fReadOnly = False):
     50    ksOnSubmit_AddReturnToFieldWithCurrentUrl = '+AddReturnToFieldWithCurrentUrl+';
     51
     52    def __init__(self, sId, sAction, dErrors = None, fReadOnly = False, sOnSubmit = None):
    5153        self._fFinalized = False;
    5254        self._fReadOnly  = fReadOnly;
    5355        self._dErrors    = dErrors if dErrors is not None else dict();
     56
     57        if sOnSubmit == self.ksOnSubmit_AddReturnToFieldWithCurrentUrl:
     58            sOnSubmit = 'return addRedirectToInputFieldWithCurrentUrl(this)';
     59        if sOnSubmit is None:   sOnSubmit = u'';
     60        else:                   sOnSubmit = u' onsubmit=\"%s\"' % (escapeAttr(sOnSubmit),);
     61
    5462        self._sBody      = u'\n' \
    5563                           u'<div id="%s" class="tmform">\n' \
    56                            u'  <form action="%s" method="post">\n' \
     64                           u'  <form action="%s" method="post"%s>\n' \
    5765                           u'    <ul>\n' \
    58                          % (sId, sAction);
     66                         % (sId, sAction, sOnSubmit);
    5967
    6068    def _add(self, sText):
     
    7078        if sText.find('<br>') >= 0:
    7179            asParts = sText.split('<br>');
    72             for i in range(len(asParts)):
     80            for i, _ in enumerate(asParts):
    7381                asParts[i] = escapeElem(asParts[i].strip());
    7482            sText = '<br>\n'.join(asParts);
     
    114122                         '    </li>\n'
    115123                         % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeElem(str(sValue)) ));
     124    #
     125    # Non-input stuff.
     126    #
     127    def addNonText(self, sValue, sLabel, sPostHtml = ''):
     128        """Adds a read-only text input."""
     129        self._addLabel('non-text', sLabel, 'string');
     130        if sValue is None: sValue = '';
     131        return self._add('          <p>%s</p>%s\n'
     132                         '        </div></div>\n'
     133                         '      </li>\n'
     134                         % (escapeElem(str(sValue)), sPostHtml ));
     135
    116136
    117137    #
     
    124144        if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
    125145        self._addLabel(sName, sLabel, sSubClass);
     146        if sValue is None: sValue = '';
    126147        return self._add('          <input name="%s" id="%s" type="text"%s value="%s">%s\n'
    127148                         '        </div></div>\n'
     
    133154        if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
    134155        self._addLabel(sName, sLabel, sSubClass);
     156        if sValue is None: sValue = '';
    135157        return self._add('          <input name="%s" id="%s" type="text" readonly%s value="%s" class="tmform-input-readonly">%s\n'
    136158                         '        </div></div>\n'
     
    153175        if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
    154176        self._addLabel(sName, sLabel, sSubClass)
     177        if sValue is None: sValue = '';
    155178        sNewValue = str(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
    156179        return self._add('          <textarea name="%s" id="%s" %s>%s</textarea>\n'
     
    163186        if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
    164187        self._addLabel(sName, sLabel, sSubClass)
     188        if sValue is None: sValue = '';
    165189        sNewValue = str(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
    166190        return self._add('          <textarea name="%s" id="%s" readonly %s>%s</textarea>\n'
     
    213237        self._add('          <select name="%s" id="%s" class="tmform-combobox"%s>\n'
    214238                  % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
     239        sSelected = str(sSelected);
    215240        for iValue, sText, _ in aoOptions:
    216241            sValue = str(iValue);
    217242            self._add('            <option value="%s"%s>%s</option>\n'
    218                       % (escapeAttr(sValue), ' selected' if sValue == str(sSelected) else '',
     243                      % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
    219244                         escapeElem(sText)));
    220245        return self._add('          </select>\n'
     
    229254        self._add('          <select name="%s" id="%s" disabled class="tmform-combobox"%s>\n'
    230255                  % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
     256        sSelected = str(sSelected);
    231257        for iValue, sText, _ in aoOptions:
    232258            sValue = str(iValue);
    233259            self._add('            <option value="%s"%s>%s</option>\n'
    234                       % (escapeAttr(sValue), ' selected' if sValue == str(sSelected) else '',
     260                      % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
    235261                         escapeElem(sText)));
    236262        return self._add('          </select>\n'
     
    526552            dSubErrors = self._dErrors[sName];
    527553
    528         for iVar in range(len(aoVariations)):
     554        for iVar, _ in enumerate(aoVariations):
    529555            oVar = copy.copy(aoVariations[iVar]);
    530556            oVar.convertToParamNull();
     
    623649        oDefMember = TestGroupMemberData();
    624650        aoTestGroupMembers = list(aoTestGroupMembers); # Copy it so we can pop.
    625         for iTestCase in range(len(aoAllTestCases)):
     651        for iTestCase, _ in enumerate(aoAllTestCases):
    626652            oTestCase = aoAllTestCases[iTestCase];
    627653
    628654            # Is it a member?
    629655            oMember = None;
    630             for i in range(len(aoTestGroupMembers)):
     656            for i, _ in enumerate(aoTestGroupMembers):
    631657                if aoTestGroupMembers[i].oTestCase.idTestCase == oTestCase.idTestCase:
    632658                    oMember = aoTestGroupMembers.pop(i);
     
    735761        oDefMember = SchedGroupMemberData();
    736762        aoSchedGroupMembers = list(aoSchedGroupMembers); # Copy it so we can pop.
    737         for iTestGroup in range(len(aoAllTestGroups)):
     763        for iTestGroup, _ in enumerate(aoAllTestGroups):
    738764            oTestGroup = aoAllTestGroups[iTestGroup];
    739765
    740766            # Is it a member?
    741767            oMember = None;
    742             for i in range(len(aoSchedGroupMembers)):
     768            for i, _ in enumerate(aoSchedGroupMembers):
    743769                if aoSchedGroupMembers[i].oTestGroup.idTestGroup == oTestGroup.idTestGroup:
    744770                    oMember = aoSchedGroupMembers.pop(i);
  • trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py

    r56809 r61217  
    6969    ksActionResultsGroupedByTestCase    = 'ResultsGroupedByTestCase'
    7070    ksActionTestResultDetails           = 'TestResultDetails'
     71    ksActionTestResultFailureDetails    = 'TestResultFailureDetails'
     72    ksActionTestResultFailureAdd        = 'TestResultFailureAdd'
     73    ksActionTestResultFailureAddPost    = 'TestResultFailureAddPost'
     74    ksActionTestResultFailureEdit       = 'TestResultFailureEdit'
     75    ksActionTestResultFailureEditPost   = 'TestResultFailureEditPost'
    7176    ksActionViewLog                     = 'ViewLog'
    7277    ksActionGetFile                     = 'GetFile'
     
    228233                                                                WuiGroupedResultList)
    229234
    230         d[self.ksActionTestResultDetails]          = self.actionTestResultDetails
     235        d[self.ksActionTestResultDetails]          = self._actionTestResultDetails;
     236
     237        d[self.ksActionTestResultFailureAdd]       = self._actionTestResultFailureAdd;
     238        d[self.ksActionTestResultFailureAddPost]   = self._actionTestResultFailureAddPost;
     239        d[self.ksActionTestResultFailureDetails]   = self._actionTestResultFailureDetails;
     240        d[self.ksActionTestResultFailureEdit]      = self._actionTestResultFailureEdit;
     241        d[self.ksActionTestResultFailureEditPost]  = self._actionTestResultFailureEditPost;
    231242
    232243        d[self.ksActionViewLog]                 = self.actionViewLog;
     
    813824        return WuiDispatcherBase._generatePage(self)
    814825
    815     def actionTestResultDetails(self):
     826    def _actionTestResultDetails(self):
    816827        """Show test case execution result details."""
    817828        from testmanager.webui.wuitestresult import WuiTestResult;
     
    849860        return True
    850861
     862    def _actionTestResultFailureAdd(self):
     863        """ Pro forma. """
     864        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
     865        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
     866        return self._actionGenericFormAdd(TestResultFailureData, WuiTestResultFailure);
     867
     868    def _actionTestResultFailureAddPost(self):
     869        """Add test result failure result"""
     870        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
     871        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
     872        if self.ksParamRedirectTo not in self._dParams:
     873            raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
     874
     875        return self._actionGenericFormAddPost(TestResultFailureData, TestResultFailureLogic,
     876                                              WuiTestResultFailure, self.ksActionResultsUnGrouped);
     877
     878    def _actionTestResultFailureDetails(self):
     879        """ Pro forma. """
     880        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
     881        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
     882        return self._actionGenericFormDetails(TestResultFailureData, TestResultFailureLogic,
     883                                              WuiTestResultFailure, 'idTestResult');
     884
     885    def _actionTestResultFailureEdit(self):
     886        """ Pro forma. """
     887        from testmanager.core.testresults import TestResultFailureData;
     888        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
     889        return self._actionGenericFormEdit(TestResultFailureData, WuiTestResultFailure,
     890                                           TestResultFailureData.ksParam_idTestResult);
     891
     892    def _actionTestResultFailureEditPost(self):
     893        """Edit test result failure result"""
     894        from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
     895        from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
     896        if self.ksParamRedirectTo not in self._dParams:
     897            raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
     898
     899        return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic,
     900                                               WuiTestResultFailure, self.ksActionResultsUnGrouped);
     901
    851902    def actionViewLog(self):
    852903        """
  • trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py

    r56806 r61217  
    3535                                               WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml;
    3636from testmanager.webui.wuimain          import WuiMain;
     37from testmanager.webui.wuihlpform       import WuiHlpForm;
     38from testmanager.core.failurereason     import FailureReasonData, FailureReasonLogic;
    3739from testmanager.core.report            import ReportGraphModel;
    3840from testmanager.core.testbox           import TestBoxData;
     
    4042from testmanager.core.testset           import TestSetData;
    4143from testmanager.core.testgroup         import TestGroupData;
     44from testmanager.core.testresults       import TestResultFailureData;
    4245from testmanager.core.build             import BuildData;
    4346from testmanager.core                   import db;
     
    137140            sErrCnt = ' (1 error)' if oTestResult.cErrors == 1 else ' (%d errors)' % oTestResult.cErrors;
    138141
     142        # Format bits for adding or editing the failure reason.  Level 0 is handled at the top of the page.
     143        sChangeReason = '';
     144        if oTestResult.cErrors > 0 and iDepth > 0:
     145            dTmp = {
     146                self._oDisp.ksParamAction: self._oDisp.ksActionTestResultFailureAdd if oTestResult.oReason is None else
     147                                           self._oDisp.ksActionTestResultFailureEdit,
     148                TestResultFailureData.ksParam_idTestResult: oTestResult.idTestResult,
     149            };
     150            sChangeReason = ' <a href="?%s" class="tmtbl-edit-reason" onclick="addRedirectToAnchorHref(this)">%s</a> ' \
     151                          % ( webutils.encodeUrlParams(dTmp), WuiContentBase.ksShortEditLinkHtml );
     152
    139153        # Format the include in graph checkboxes.
    140154        sLineage += ':%u' % (oTestResult.idStrName,);
     
    148162
    149163        if    len(oTestResult.aoChildren) == 0 \
    150           and len(oTestResult.aoValues)   == 0 \
    151           and len(oTestResult.aoMsgs)     == 0 \
    152           and len(oTestResult.aoFiles)    == 0:
     164          and len(oTestResult.aoValues) + len(oTestResult.aoMsgs) + len(oTestResult.aoFiles) == 0:
    153165            # Leaf - single row.
    154166            tsEvent = oTestResult.tsCreated;
     
    160172                     '  <td>%s</td>\n' \
    161173                     '  <td>%s</td>\n' \
    162                      '  <td colspan="2"%s>%s%s</td>\n' \
     174                     '  <td colspan="2"%s>%s%s%s</td>\n' \
    163175                     '  <td>%s</td>\n' \
    164176                     ' </tr>\n' \
     
    170182                       sDisplayName,
    171183                       ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '',
    172                        webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt),
     184                       webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt), sChangeReason,
    173185                       sResultGraph );
    174186            iRow += 1;
     
    189201            iRow += 1;
    190202
    191             # Depth.
     203            # Depth. Check if our error count is just reflecting the one of our children.
     204            cErrorsBelow = 0;
    192205            for oChild in oTestResult.aoChildren:
    193206                (sChildHtml, iRow, iFailure) = self._recursivelyGenerateEvents(oChild, sName, sLineage,
    194207                                                                               iRow, iFailure, oTestSet, iDepth + 1);
    195208                sHtml += sChildHtml;
    196 
     209                cErrorsBelow += oChild.cErrors;
     210
     211            if cErrorsBelow >= oTestResult.cErrors:
     212                sChangeReason = '';
    197213
    198214            # Messages.
     
    263279
    264280                sHtml += ' <tr class="%s tmtbl-events-file tmtbl-events-lvl%s">\n' \
    265                          '  <td></td>\n' \
    266                          '  <td>%s</td>\n' \
     281                         '  <td>%s</td>\n' \
     282                         '  <td></td>\n' \
    267283                         '  <td></td>\n' \
    268284                         '  <td>%s</td>\n' \
     
    296312                iRow += 1;
    297313
     314        # Failure reason.
     315        if oTestResult.oReason is not None:
     316            sReasonText = '%s / %s' % (     oTestResult.oReason.oFailureReason.oCategory.sShort,
     317                                            oTestResult.oReason.oFailureReason.sShort, );
     318            sCommentHtml = '';
     319            if oTestResult.oReason.sComment is not None and len(oTestResult.oReason.sComment.strip()) > 0:
     320                sCommentHtml = '<br>' + webutils.escapeElem(oTestResult.oReason.sComment.strip());
     321                sCommentHtml = sCommentHtml.replace('\n', '<br>');
     322
     323            sDetailedReason = ' <a href="?%s" class="tmtbl-show-reason">%s</a>' \
     324                            % ( webutils.encodeUrlParams({ self._oDisp.ksParamAction:
     325                                                           self._oDisp.ksActionTestResultFailureDetails,
     326                                                           TestResultFailureData.ksParam_idTestResult:
     327                                                           oTestResult.idTestResult,}),
     328                                WuiContentBase.ksShortDetailsLinkHtml,);
     329
     330
     331            sHtml += ' <tr class="%s tmtbl-events-reason tmtbl-events-lvl%s">\n' \
     332                     '  <td>%s</td>\n' \
     333                     '  <td colspan="2">%s</td>\n' \
     334                     '  <td colspan="3">%s%s%s%s</td>\n' \
     335                     '  <td>%s</td>\n' \
     336                     ' </tr>\n' \
     337                   % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
     338                        webutils.escapeElem(self.formatTsShort(oTestResult.oReason.tsEffective)),
     339                        oTestResult.oReason.oAuthor.sUsername,
     340                        webutils.escapeElem(sReasonText), sDetailedReason, sChangeReason,
     341                        sCommentHtml,
     342                       'todo');
     343            iRow += 1;
     344
    298345        if oTestResult.isFailure():
    299346            iFailure += 1;
    300347
    301348        return (sHtml, iRow, iFailure);
     349
     350
     351    def _generateMainReason(self, oTestResultTree, oTestSet):
     352        """
     353        Generates the form for displaying and updating the main failure reason.
     354
     355        oTestResultTree is an instance TestResultDataEx.
     356        oTestSet is an instance of TestSetData.
     357
     358        """
     359        _ = oTestSet;
     360        sHtml = ' ';
     361
     362        if oTestResultTree.isFailure() or oTestResultTree.cErrors > 0:
     363            sHtml += '   <h2>Failure Reason:</h2>\n';
     364            oData = oTestResultTree.oReason;
     365
     366            # We need the failure reasons for the combobox.
     367            aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why');
     368            assert len(aoFailureReasons) > 0;
     369
     370            # For now we'll use the standard form helper.
     371            sFormActionUrl = '%s?%s=%s' % ( self._oDisp.ksScriptName, self._oDisp.ksParamAction,
     372                                            WuiMain.ksActionTestResultFailureAddPost if oData is None else
     373                                            WuiMain.ksActionTestResultFailureEditPost )
     374            oForm = WuiHlpForm('failure-reason', sFormActionUrl,
     375                               sOnSubmit = WuiHlpForm.ksOnSubmit_AddReturnToFieldWithCurrentUrl);
     376            oForm.addTextHidden(TestResultFailureData.ksParam_idTestResult, oTestResultTree.idTestResult);
     377            oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason if oData is not None else -1,
     378                              'Reason', aoFailureReasons);
     379            oForm.addMultilineText(TestResultFailureData.ksParam_sComment,
     380                                   oData.sComment if oData is not None else '', 'Comment');
     381            if oData is not None:
     382                oForm.addNonText('%s (%s)' % (oData.oAuthor.sUsername, oData.oAuthor.sUsername), 'Sheriff');
     383                oForm.addNonText(oData.tsEffective, 'When');
     384                oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, oData.tsEffective);
     385                oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, oData.tsExpire);
     386                oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor);
     387            else:
     388                oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, '');
     389                oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, '');
     390                oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, '');
     391
     392            oForm.addSubmit('Change Reason', );
     393            sHtml += oForm.finalize();
     394        return sHtml;
     395
    302396
    303397    def showTestCaseResultDetails(self,             # pylint: disable=R0914,R0915
     
    534628
    535629        sHtml += '  <td valign="top" width="80%" style="padding-left:6px">\n';
     630        sHtml += self._generateMainReason(oTestResultTree, oTestSet);
     631
    536632        sHtml += '   <h2>Events:</h2>\n';
    537633        sHtml += '   <form action="#" method="get" id="graph-form">\n' \
     
    614710            'Start',
    615711            'Product Build',
    616             'Validation Kit',
    617             'TestBox OS',
    618             'TestBox Name',
     712            'Kit',
     713            'Box',
     714            'OS.Arch',
    619715            'Test Case',
    620716            'Elapsed',
    621717            'Result',
     718            'Reason',
    622719        ];
    623720        self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"',
    624721                                 'align="center"', 'align="center"', 'align="center"',
    625722                                 'align="center"', 'align="center"', 'align="center"',
    626                                  'align="center"', 'align="center"', 'align="center"' ]
     723                                 'align="center"', 'align="center"', 'align="center"',
     724                                 'align="center"', ];
    627725
    628726
     
    650748        oValidationKit = None;
    651749        if oEntry.idBuildTestSuite is not None:
    652             oValidationKit = WuiTmLink('#%d - r%s' % (oEntry.idBuildTestSuite, oEntry.iRevisionTestSuite),
     750            oValidationKit = WuiTmLink('r%s' % (oEntry.iRevisionTestSuite,),
    653751                                   WuiAdmin.ksScriptName,
    654752                                   { WuiAdmin.ksParamAction:  WuiAdmin.ksActionBuildDetails,
     
    656754                                   fBracketed = False);
    657755
    658 
    659         aoTestSetLinks = [ WuiTmLink(oEntry.enmStatus,
    660                                      WuiMain.ksScriptName,
    661                                      { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
    662                                        TestSetData.ksParam_idTestSet: oEntry.idTestSet },
    663                                      fBracketed = False),];
     756        aoTestSetLinks = [];
     757        aoTestSetLinks.append(WuiTmLink(oEntry.enmStatus,
     758                                        WuiMain.ksScriptName,
     759                                        { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
     760                                          TestSetData.ksParam_idTestSet: oEntry.idTestSet },
     761                                        fBracketed = False));
    664762        if oEntry.cErrors > 0:
    665             aoTestSetLinks.append(WuiTmLink('- %d error(s)' % (oEntry.cErrors, ),
     763            aoTestSetLinks.append(WuiRawHtml('-'));
     764            aoTestSetLinks.append(WuiTmLink('%d error%s' % (oEntry.cErrors, '' if oEntry.cErrors == 1 else 's', ),
    666765                                            WuiMain.ksScriptName,
    667766                                            { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
     
    688787        sTestBoxTitle += u'CPU features:\t' + u', '.join(asFeatures);
    689788
     789        # Reason:
     790        oReason = None;
     791        if oEntry.oFailureReason is not None:
     792            sReasonTitle  = 'Reason:  \t%s\n' % ( oEntry.oFailureReason.sShort, );
     793            sReasonTitle += 'Category:\t%s\n' % ( oEntry.oFailureReason.oCategory.sShort, );
     794            sReasonTitle += 'Assigned:\t%s\n' % ( self.formatTsShort(oEntry.tsFailureReasonAssigned), );
     795            sReasonTitle += 'By User: \t%s\n' % ( oEntry.oFailureReasonAssigner.sUsername, );
     796            if oEntry.sFailureReasonComment is not None and len(oEntry.sFailureReasonComment) > 0:
     797                sReasonTitle += 'Comment: \t%s\n' % ( self.formatTsShort(oEntry.sFailureReasonComment), );
     798            if oEntry.oFailureReason.iTicket is not None and oEntry.oFailureReason.iTicket > 0:
     799                sReasonTitle += 'xTracker:\t#%s\n' % ( oEntry.oFailureReason.iTicket, );
     800            for i, sUrl in enumerate(oEntry.oFailureReason.asUrls):
     801                sUrl = sUrl.strip();
     802                if len(sUrl) > 0:
     803                    sReasonTitle += 'URL#%u:  \t%s\n' % ( i, sUrl, );
     804            oReason = WuiTmLink(oEntry.oFailureReason.sShort, WuiAdmin.ksScriptName,
     805                                { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
     806                                  FailureReasonData.ksParam_idFailureReason: oEntry.oFailureReason.idFailureReason },
     807                                sTitle = sReasonTitle);
     808
    690809        return [
    691810            oEntry.tsCreated,
    692             [ WuiTmLink('#%d - %s %s (%s)' % (oEntry.idBuild, oEntry.sProduct, oEntry.sVersion, oEntry.sType,),
     811            [ WuiTmLink('%s %s (%s)' % (oEntry.sProduct, oEntry.sVersion, oEntry.sType,),
    693812                        WuiMain.ksScriptName, self._dRevLinkParams, sTitle = '%s' % (oEntry.sBranch,), fBracketed = False),
    694813              WuiSvnLinkWithTooltip(oEntry.iRevision, 'vbox'), ## @todo add sRepository TestResultListingData
     
    699818              ],
    700819            oValidationKit,
    701             '%s.%s' % (oEntry.sOs, oEntry.sArch),
    702820            [ WuiTmLink(oEntry.sTestBoxName, WuiMain.ksScriptName, self._dTestBoxLinkParams, fBracketed = False,
    703821                        sTitle = sTestBoxTitle),
     
    706824                          TestBoxData.ksParam_idTestBox: oEntry.idTestBox },
    707825                        fBracketed = False) ],
     826            '%s.%s' % (oEntry.sOs, oEntry.sArch),
    708827            [ WuiTmLink(oEntry.sTestCaseName, WuiMain.ksScriptName, self._dTestCaseLinkParams, fBracketed = False,
    709828                        sTitle = (oEntry.sBaseCmd + ' ' + oEntry.sArgs) if oEntry.sArgs else oEntry.sBaseCmd),
     
    713832                        fBracketed = False), ],
    714833            oEntry.tsElapsed,
    715             aoTestSetLinks
     834            aoTestSetLinks,
     835            oReason
    716836        ];
Note: See TracChangeset for help on using the changeset viewer.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette