VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py@ 106670

Last change on this file since 106670 was 106061, checked in by vboxsync, 5 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 25.1 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: failurereason.py 106061 2024-09-16 14:03:52Z vboxsync $
3
4"""
5Test Manager - Failure Reasons.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2024 Oracle and/or its affiliates.
11
12This file is part of VirtualBox base platform packages, as
13available from https://www.virtualbox.org.
14
15This program is free software; you can redistribute it and/or
16modify it under the terms of the GNU General Public License
17as published by the Free Software Foundation, in version 3 of the
18License.
19
20This program is distributed in the hope that it will be useful, but
21WITHOUT ANY WARRANTY; without even the implied warranty of
22MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23General Public License for more details.
24
25You should have received a copy of the GNU General Public License
26along with this program; if not, see <https://www.gnu.org/licenses>.
27
28The contents of this file may alternatively be used under the terms
29of the Common Development and Distribution License Version 1.0
30(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
31in the VirtualBox distribution, in which case the provisions of the
32CDDL are applicable instead of those of the GPL.
33
34You may elect to license modified versions of this file under the
35terms and conditions of either the GPL or the CDDL or both.
36
37SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
38"""
39__version__ = "$Revision: 106061 $"
40
41
42# Standard Python imports.
43import sys;
44
45# Validation Kit imports.
46from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \
47 AttributeChangeEntry, ChangeLogEntry;
48from testmanager.core.useraccount import UserAccountLogic;
49
50# Python 3 hacks:
51if sys.version_info[0] >= 3:
52 xrange = range; # pylint: disable=redefined-builtin,invalid-name
53
54
55class FailureReasonData(ModelDataBase):
56 """
57 Failure Reason Data.
58 """
59
60 ksIdAttr = 'idFailureReason';
61
62 ksParam_idFailureReason = 'FailureReasonData_idFailureReason'
63 ksParam_tsEffective = 'FailureReasonData_tsEffective'
64 ksParam_tsExpire = 'FailureReasonData_tsExpire'
65 ksParam_uidAuthor = 'FailureReasonData_uidAuthor'
66 ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory'
67 ksParam_sShort = 'FailureReasonData_sShort'
68 ksParam_sFull = 'FailureReasonData_sFull'
69 ksParam_iTicket = 'FailureReasonData_iTicket'
70 ksParam_asUrls = 'FailureReasonData_asUrls'
71
72 kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire',
73 'uidAuthor', 'iTicket', 'asUrls' ]
74
75 def __init__(self):
76 ModelDataBase.__init__(self);
77
78 #
79 # Initialize with defaults.
80 # See the database for explanations of each of these fields.
81 #
82
83 self.idFailureReason = None
84 self.tsEffective = None
85 self.tsExpire = None
86 self.uidAuthor = None
87 self.idFailureCategory = None
88 self.sShort = None
89 self.sFull = None
90 self.iTicket = None
91 self.asUrls = None
92
93 def initFromDbRow(self, aoRow):
94 """
95 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
96
97 Returns self. Raises exception if the row is None or otherwise invalid.
98 """
99
100 if aoRow is None:
101 raise TMRowNotFound('Failure Reason not found.');
102
103 self.idFailureReason = aoRow[0]
104 self.tsEffective = aoRow[1]
105 self.tsExpire = aoRow[2]
106 self.uidAuthor = aoRow[3]
107 self.idFailureCategory = aoRow[4]
108 self.sShort = aoRow[5]
109 self.sFull = aoRow[6]
110 self.iTicket = aoRow[7]
111 self.asUrls = aoRow[8]
112
113 return self;
114
115 def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None):
116 """
117 Initialize from the database, given the ID of a row.
118 """
119 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
120 'SELECT *\n'
121 'FROM FailureReasons\n'
122 'WHERE idFailureReason = %s\n'
123 , ( idFailureReason,), tsNow, sPeriodBack));
124 aoRow = oDb.fetchOne()
125 if aoRow is None:
126 raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)'
127 % (idFailureReason, tsNow, sPeriodBack,));
128 return self.initFromDbRow(aoRow);
129
130
131class FailureReasonDataEx(FailureReasonData):
132 """
133 Failure Reason Data, extended version that includes the category.
134 """
135
136 def __init__(self):
137 FailureReasonData.__init__(self);
138 self.oCategory = None;
139 self.oAuthor = None;
140
141 def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
142 """
143 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
144
145 Returns self. Raises exception if the row is None or otherwise invalid.
146 """
147
148 self.initFromDbRow(aoRow);
149 self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory);
150 self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
151
152 return self;
153
154
155class FailureReasonLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
156 """
157 Failure Reason logic.
158 """
159
160 def __init__(self, oDb):
161 ModelLogicBase.__init__(self, oDb)
162 self.dCache = None;
163 self.dCacheNameAndCat = None;
164 self.oCategoryLogic = None;
165 self.oUserAccountLogic = None;
166
167 def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
168 """
169 Fetches Failure Category records.
170
171 Returns an array (list) of FailureReasonDataEx items, empty list if none.
172 Raises exception on error.
173 """
174 _ = aiSortColumns;
175 self._ensureCachesPresent();
176
177 if tsNow is None:
178 self._oDb.execute('SELECT FailureReasons.*,\n'
179 ' FailureCategories.sShort AS sCategory\n'
180 'FROM FailureReasons,\n'
181 ' FailureCategories\n'
182 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
183 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
184 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
185 'ORDER BY sCategory ASC, sShort ASC\n'
186 'LIMIT %s OFFSET %s\n'
187 , (cMaxRows, iStart,));
188 else:
189 self._oDb.execute('SELECT FailureReasons.*,\n'
190 ' FailureCategories.sShort AS sCategory\n'
191 'FROM FailureReasons,\n'
192 ' FailureCategories\n'
193 'WHERE FailureReasons.tsExpire > %s\n'
194 ' AND FailureReasons.tsEffective <= %s\n'
195 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
196 ' AND FailureReasons.tsExpire > %s\n'
197 ' AND FailureReasons.tsEffective <= %s\n'
198 'ORDER BY sCategory ASC, sShort ASC\n'
199 'LIMIT %s OFFSET %s\n'
200 , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,));
201
202 aoRows = []
203 for aoRow in self._oDb.fetchAll():
204 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
205 return aoRows
206
207 def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory, aiSortColumns = None):
208 """
209 Fetches Failure Category records.
210
211 Returns an array (list) of FailureReasonDataEx items, empty list if none.
212 Raises exception on error.
213 """
214 _ = aiSortColumns;
215 self._ensureCachesPresent();
216
217 if tsNow is None:
218 self._oDb.execute('SELECT *\n'
219 'FROM FailureReasons\n'
220 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
221 ' AND idFailureCategory = %s\n'
222 'ORDER BY sShort ASC\n'
223 'LIMIT %s OFFSET %s\n'
224 , ( idFailureCategory, cMaxRows, iStart,));
225 else:
226 self._oDb.execute('SELECT *\n'
227 'FROM FailureReasons\n'
228 'WHERE idFailureCategory = %s\n'
229 ' AND tsExpire > %s\n'
230 ' AND tsEffective <= %s\n'
231 'ORDER BY sShort ASC\n'
232 'LIMIT %s OFFSET %s\n'
233 , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,));
234
235 aoRows = []
236 for aoRow in self._oDb.fetchAll():
237 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
238 return aoRows
239
240
241 def fetchForSheriffByNamedCategory(self, sFailureCategory):
242 """
243 Fetches the short names of the reasons in the named category.
244
245 Returns array of strings.
246 Raises exception on error.
247 """
248 self._oDb.execute('SELECT FailureReasons.sShort\n'
249 'FROM FailureReasons,\n'
250 ' FailureCategories\n'
251 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
252 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
253 ' AND FailureCategories.sShort = %s\n'
254 'ORDER BY FailureReasons.sShort ASC\n'
255 , ( sFailureCategory,));
256 return [aoRow[0] for aoRow in self._oDb.fetchAll()];
257
258
259 def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
260 """
261 Gets the list of Failure Reasons for a combo box.
262 Returns an array of (value [idFailureReason], drop-down-name [sShort],
263 hover-text [sFull]) tuples.
264 """
265 if tsEffective is None:
266 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
267 'FROM FailureReasons fr,\n'
268 ' FailureCategories fc\n'
269 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
270 ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n'
271 ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n'
272 'ORDER BY sComboText')
273 else:
274 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
275 'FROM FailureReasons fr,\n'
276 ' FailureCategories fc\n'
277 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
278 ' AND fr.tsExpire > %s\n'
279 ' AND fr.tsEffective <= %s\n'
280 ' AND fc.tsExpire > %s\n'
281 ' AND fc.tsEffective <= %s\n'
282 'ORDER BY sComboText'
283 , (tsEffective, tsEffective, tsEffective, tsEffective));
284 aoRows = self._oDb.fetchAll();
285 return [(-1, sFirstEntry, '')] + aoRows;
286
287
288 def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
289 """
290 Fetches change log entries for a failure reason.
291
292 Returns an array of ChangeLogEntry instance and an indicator whether
293 there are more entries.
294 Raises exception on error.
295 """
296 self._ensureCachesPresent();
297
298 if tsNow is None:
299 tsNow = self._oDb.getCurrentTimestamp();
300
301 # 1. Get a list of the relevant changes.
302 self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n'
303 'ORDER BY tsEffective DESC\n'
304 'LIMIT %s OFFSET %s\n'
305 , ( idFailureReason, tsNow, cMaxRows + 1, iStart, ));
306 aoRows = [];
307 for aoChange in self._oDb.fetchAll():
308 aoRows.append(FailureReasonData().initFromDbRow(aoChange));
309
310 # 2. Calculate the changes.
311 aoEntries = [];
312 for i in xrange(0, len(aoRows) - 1):
313 oNew = aoRows[i];
314 oOld = aoRows[i + 1];
315
316 aoChanges = [];
317 for sAttr in oNew.getDataAttributes():
318 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
319 oOldAttr = getattr(oOld, sAttr);
320 oNewAttr = getattr(oNew, sAttr);
321 if oOldAttr != oNewAttr:
322 if sAttr == 'idFailureCategory':
323 oCat = self.oCategoryLogic.cachedLookup(oOldAttr);
324 if oCat is not None:
325 oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
326 oCat = self.oCategoryLogic.cachedLookup(oNewAttr);
327 if oCat is not None:
328 oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
329 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
330
331 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
332
333 # If we're at the end of the log, add the initial entry.
334 if len(aoRows) <= cMaxRows and aoRows:
335 oNew = aoRows[-1];
336 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
337
338 return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
339
340
341 def getById(self, idFailureReason):
342 """Get Failure Reason data by idFailureReason"""
343
344 self._oDb.execute('SELECT *\n'
345 'FROM FailureReasons\n'
346 'WHERE tsExpire = \'infinity\'::timestamp\n'
347 ' AND idFailureReason = %s;', (idFailureReason,))
348 aRows = self._oDb.fetchAll()
349 if len(aRows) not in (0, 1):
350 raise self._oDb.integrityException(
351 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
352 try:
353 return FailureReasonData().initFromDbRow(aRows[0])
354 except IndexError:
355 return None
356
357
358 def addEntry(self, oData, uidAuthor, fCommit = False):
359 """
360 Add a failure reason.
361 """
362 #
363 # Validate.
364 #
365 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
366 if dErrors:
367 raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
368
369 #
370 # Add the record.
371 #
372 self._readdEntry(uidAuthor, oData);
373 self._oDb.maybeCommit(fCommit);
374 return True;
375
376
377 def editEntry(self, oData, uidAuthor, fCommit = False):
378 """
379 Modifies a failure reason.
380 """
381
382 #
383 # Validate inputs and read in the old(/current) data.
384 #
385 assert isinstance(oData, FailureReasonData);
386 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
387 if dErrors:
388 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
389
390 oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason);
391
392 #
393 # Update the data that needs updating.
394 #
395 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
396 self._historizeEntry(oData.idFailureReason);
397 self._readdEntry(uidAuthor, oData);
398 self._oDb.maybeCommit(fCommit);
399 return True;
400
401
402 def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False):
403 """
404 Deletes a failure reason.
405 """
406 _ = fCascade; # too complicated for now.
407
408 #
409 # Check whether it's being used by other tables and bitch if it is .
410 # We currently do not implement cascading.
411 #
412 self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n'
413 'FROM BuildBlacklist\n'
414 'WHERE idFailureReason = %s\n'
415 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
416 'UNION\n'
417 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n'
418 'FROM TestResultFailures\n'
419 'WHERE idFailureReason = %s\n'
420 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
421 , (idFailureReason, idFailureReason,));
422 aaoRows = self._oDb.fetchAll();
423 if aaoRows:
424 raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s'
425 % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),));
426
427 #
428 # Do the job.
429 #
430 oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason);
431 assert oData.idFailureReason == idFailureReason;
432 (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
433 if oData.tsEffective not in (tsCur, tsCurMinusOne):
434 self._historizeEntry(idFailureReason, tsCurMinusOne);
435 self._readdEntry(uidAuthor, oData, tsCurMinusOne);
436 self._historizeEntry(idFailureReason);
437 self._oDb.maybeCommit(fCommit);
438 return True;
439
440
441 def cachedLookup(self, idFailureReason):
442 """
443 Looks up the most recent FailureReasonDataEx object for idFailureReason
444 via an object cache.
445
446 Returns a shared FailureReasonData object. None if not found.
447 Raises exception on DB error.
448 """
449 if self.dCache is None:
450 self.dCache = self._oDb.getCache('FailureReasonDataEx');
451 oEntry = self.dCache.get(idFailureReason, None);
452 if oEntry is None:
453 self._oDb.execute('SELECT *\n'
454 'FROM FailureReasons\n'
455 'WHERE idFailureReason = %s\n'
456 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
457 , (idFailureReason, ));
458 if self._oDb.getRowCount() == 0:
459 # Maybe it was deleted, try get the last entry.
460 self._oDb.execute('SELECT *\n'
461 'FROM FailureReasons\n'
462 'WHERE idFailureReason = %s\n'
463 'ORDER BY tsExpire DESC\n'
464 'LIMIT 1\n'
465 , (idFailureReason, ));
466 elif self._oDb.getRowCount() > 1:
467 raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
468
469 if self._oDb.getRowCount() == 1:
470 self._ensureCachesPresent();
471 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
472 self.oUserAccountLogic);
473 self.dCache[idFailureReason] = oEntry;
474 return oEntry;
475
476
477 def cachedLookupByNameAndCategory(self, sName, sCategory):
478 """
479 Looks up a failure reason by it's name and category.
480
481 Should the request be ambigiuos, we'll return the oldest one.
482
483 Returns a shared FailureReasonData object. None if not found.
484 Raises exception on DB error.
485 """
486 if self.dCacheNameAndCat is None:
487 self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
488 sKey = '%s:::%s' % (sName, sCategory,);
489 oEntry = self.dCacheNameAndCat.get(sKey, None);
490 if oEntry is None:
491 self._oDb.execute('SELECT *\n'
492 'FROM FailureReasons,\n'
493 ' FailureCategories\n'
494 'WHERE FailureReasons.sShort = %s\n'
495 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
496 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory '
497 ' AND FailureCategories.sShort = %s\n'
498 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
499 'ORDER BY FailureReasons.tsEffective\n'
500 , ( sName, sCategory));
501 if self._oDb.getRowCount() == 0:
502 sLikeSucks = self._oDb.formatBindArgs(
503 'SELECT *\n'
504 'FROM FailureReasons,\n'
505 ' FailureCategories\n'
506 'WHERE ( FailureReasons.sShort ILIKE @@@@@@@! %s !@@@@@@@\n'
507 ' OR FailureReasons.sFull ILIKE @@@@@@@! %s !@@@@@@@)\n'
508 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
509 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
510 ' AND ( FailureCategories.sShort = %s\n'
511 ' OR FailureCategories.sFull = %s)\n'
512 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
513 'ORDER BY FailureReasons.tsEffective\n'
514 , ( sName, sName, sCategory, sCategory ));
515 sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\'');
516 self._oDb.execute(sLikeSucks);
517 if self._oDb.getRowCount() > 0:
518 self._ensureCachesPresent();
519 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
520 self.oUserAccountLogic);
521 self.dCacheNameAndCat[sKey] = oEntry;
522 if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort:
523 sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,);
524 self.dCacheNameAndCat[sKey2] = oEntry;
525 return oEntry;
526
527
528 #
529 # Helpers.
530 #
531
532 def _readdEntry(self, uidAuthor, oData, tsEffective = None):
533 """
534 Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry.
535 """
536 if tsEffective is None:
537 tsEffective = self._oDb.getCurrentTimestamp();
538 self._oDb.execute('INSERT INTO FailureReasons (\n'
539 ' uidAuthor,\n'
540 ' tsEffective,\n'
541 ' idFailureReason,\n'
542 ' idFailureCategory,\n'
543 ' sShort,\n'
544 ' sFull,\n'
545 ' iTicket,\n'
546 ' asUrls)\n'
547 'VALUES (%s, %s, '
548 + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) )
549 + ', %s, %s, %s, %s, %s)\n'
550 , ( uidAuthor,
551 tsEffective,
552 oData.idFailureCategory,
553 oData.sShort,
554 oData.sFull,
555 oData.iTicket,
556 oData.asUrls,) );
557 return True;
558
559
560 def _historizeEntry(self, idFailureReason, tsExpire = None):
561 """ Historizes the current entry. """
562 if tsExpire is None:
563 tsExpire = self._oDb.getCurrentTimestamp();
564 self._oDb.execute('UPDATE FailureReasons\n'
565 'SET tsExpire = %s\n'
566 'WHERE idFailureReason = %s\n'
567 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
568 , (tsExpire, idFailureReason,));
569 return True;
570
571
572 def _ensureCachesPresent(self):
573 """ Ensures we've got the cache references resolved. """
574 if self.oCategoryLogic is None:
575 from testmanager.core.failurecategory import FailureCategoryLogic;
576 self.oCategoryLogic = FailureCategoryLogic(self._oDb);
577 if self.oUserAccountLogic is None:
578 self.oUserAccountLogic = UserAccountLogic(self._oDb);
579 return True;
580
Note: See TracBrowser for help on using the repository browser.

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