VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/testset.py@ 98103

Last change on this file since 98103 was 98103, checked in by vboxsync, 2 years ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 37.3 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testset.py 98103 2023-01-17 14:15:46Z vboxsync $
3
4"""
5Test Manager - TestSet.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2023 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: 98103 $"
40
41
42# Standard python imports.
43import os;
44import zipfile;
45import unittest;
46
47# Validation Kit imports.
48from common import utils;
49from testmanager import config;
50from testmanager.core import db;
51from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, \
52 TMExceptionBase, TMTooManyRows, TMRowNotFound;
53from testmanager.core.testbox import TestBoxData;
54from testmanager.core.testresults import TestResultFileDataEx;
55
56
57class TestSetData(ModelDataBase):
58 """
59 TestSet Data.
60 """
61
62 ## @name TestStatus_T
63 # @{
64 ksTestStatus_Running = 'running';
65 ksTestStatus_Success = 'success';
66 ksTestStatus_Skipped = 'skipped';
67 ksTestStatus_BadTestBox = 'bad-testbox';
68 ksTestStatus_Aborted = 'aborted';
69 ksTestStatus_Failure = 'failure';
70 ksTestStatus_TimedOut = 'timed-out';
71 ksTestStatus_Rebooted = 'rebooted';
72 ## @}
73
74 ## List of relatively harmless (to testgroup/case) statuses.
75 kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];
76 ## List of bad statuses.
77 kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];
78
79 ksIdAttr = 'idTestSet';
80
81 ksParam_idTestSet = 'TestSet_idTestSet';
82 ksParam_tsConfig = 'TestSet_tsConfig';
83 ksParam_tsCreated = 'TestSet_tsCreated';
84 ksParam_tsDone = 'TestSet_tsDone';
85 ksParam_enmStatus = 'TestSet_enmStatus';
86 ksParam_idBuild = 'TestSet_idBuild';
87 ksParam_idBuildCategory = 'TestSet_idBuildCategory';
88 ksParam_idBuildTestSuite = 'TestSet_idBuildTestSuite';
89 ksParam_idGenTestBox = 'TestSet_idGenTestBox';
90 ksParam_idTestBox = 'TestSet_idTestBox';
91 ksParam_idSchedGroup = 'TestSet_idSchedGroup';
92 ksParam_idTestGroup = 'TestSet_idTestGroup';
93 ksParam_idGenTestCase = 'TestSet_idGenTestCase';
94 ksParam_idTestCase = 'TestSet_idTestCase';
95 ksParam_idGenTestCaseArgs = 'TestSet_idGenTestCaseArgs';
96 ksParam_idTestCaseArgs = 'TestSet_idTestCaseArgs';
97 ksParam_idTestResult = 'TestSet_idTestResult';
98 ksParam_sBaseFilename = 'TestSet_sBaseFilename';
99 ksParam_iGangMemberNo = 'TestSet_iGangMemberNo';
100 ksParam_idTestSetGangLeader = 'TestSet_idTestSetGangLeader';
101
102 kasAllowNullAttributes = [ 'tsDone', 'idBuildTestSuite', 'idTestSetGangLeader' ];
103 kasValidValues_enmStatus = [
104 ksTestStatus_Running,
105 ksTestStatus_Success,
106 ksTestStatus_Skipped,
107 ksTestStatus_BadTestBox,
108 ksTestStatus_Aborted,
109 ksTestStatus_Failure,
110 ksTestStatus_TimedOut,
111 ksTestStatus_Rebooted,
112 ];
113 kiMin_iGangMemberNo = 0;
114 kiMax_iGangMemberNo = 1023;
115
116
117 kcDbColumns = 20;
118
119 def __init__(self):
120 ModelDataBase.__init__(self);
121
122 #
123 # Initialize with defaults.
124 # See the database for explanations of each of these fields.
125 #
126 self.idTestSet = None;
127 self.tsConfig = None;
128 self.tsCreated = None;
129 self.tsDone = None;
130 self.enmStatus = 'running';
131 self.idBuild = None;
132 self.idBuildCategory = None;
133 self.idBuildTestSuite = None;
134 self.idGenTestBox = None;
135 self.idTestBox = None;
136 self.idSchedGroup = None;
137 self.idTestGroup = None;
138 self.idGenTestCase = None;
139 self.idTestCase = None;
140 self.idGenTestCaseArgs = None;
141 self.idTestCaseArgs = None;
142 self.idTestResult = None;
143 self.sBaseFilename = None;
144 self.iGangMemberNo = 0;
145 self.idTestSetGangLeader = None;
146
147 def initFromDbRow(self, aoRow):
148 """
149 Internal worker for initFromDbWithId and initFromDbWithGenId as well as
150 TestBoxSetLogic.
151 """
152
153 if aoRow is None:
154 raise TMRowNotFound('TestSet not found.');
155
156 self.idTestSet = aoRow[0];
157 self.tsConfig = aoRow[1];
158 self.tsCreated = aoRow[2];
159 self.tsDone = aoRow[3];
160 self.enmStatus = aoRow[4];
161 self.idBuild = aoRow[5];
162 self.idBuildCategory = aoRow[6];
163 self.idBuildTestSuite = aoRow[7];
164 self.idGenTestBox = aoRow[8];
165 self.idTestBox = aoRow[9];
166 self.idSchedGroup = aoRow[10];
167 self.idTestGroup = aoRow[11];
168 self.idGenTestCase = aoRow[12];
169 self.idTestCase = aoRow[13];
170 self.idGenTestCaseArgs = aoRow[14];
171 self.idTestCaseArgs = aoRow[15];
172 self.idTestResult = aoRow[16];
173 self.sBaseFilename = aoRow[17];
174 self.iGangMemberNo = aoRow[18];
175 self.idTestSetGangLeader = aoRow[19];
176 return self;
177
178
179 def initFromDbWithId(self, oDb, idTestSet):
180 """
181 Initialize the object from the database.
182 """
183 oDb.execute('SELECT *\n'
184 'FROM TestSets\n'
185 'WHERE idTestSet = %s\n'
186 , (idTestSet, ) );
187 aoRow = oDb.fetchOne()
188 if aoRow is None:
189 raise TMRowNotFound('idTestSet=%s not found' % (idTestSet,));
190 return self.initFromDbRow(aoRow);
191
192
193 def openFile(self, sFilename, sMode = 'rb'):
194 """
195 Opens a file.
196
197 Returns (oFile, cbFile, fIsStream) on success.
198 Returns (None, sErrorMsg, None) on failure.
199 Will not raise exceptions, unless the class instance is invalid.
200 """
201 assert sMode in [ 'rb', 'r', 'rU' ];
202
203 # Try raw file first.
204 sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename);
205 try:
206 oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding
207 return (oFile, os.fstat(oFile.fileno()).st_size, False);
208 except Exception as oXcpt1:
209 # Try the zip archive next.
210 sFile2 = os.path.join(config.g_ksZipFileAreaRootDir, self.sBaseFilename + '.zip');
211 try:
212 oZipFile = zipfile.ZipFile(sFile2, 'r'); # pylint: disable=consider-using-with
213 oFile = oZipFile.open(sFilename, sMode if sMode != 'rb' else 'r'); # pylint: disable=consider-using-with
214 cbFile = oZipFile.getinfo(sFilename).file_size;
215 return (oFile, cbFile, True);
216 except Exception as oXcpt2:
217 # Construct a meaningful error message.
218 try:
219 if os.path.exists(sFile1):
220 return (None, 'Error opening "%s": %s' % (sFile1, oXcpt1), None);
221 if not os.path.exists(sFile2):
222 return (None, 'File "%s" not found. [%s, %s]' % (sFilename, sFile1, sFile2,), None);
223 return (None, 'Error opening "%s" inside "%s": %s' % (sFilename, sFile2, oXcpt2), None);
224 except Exception as oXcpt3:
225 return (None, 'OMG! %s; %s; %s' % (oXcpt1, oXcpt2, oXcpt3,), None);
226 return (None, 'Code not reachable!', None);
227
228 def createFile(self, sFilename, sMode = 'wb'):
229 """
230 Creates a new file.
231
232 Returns oFile on success.
233 Returns sErrorMsg on failure.
234 """
235 assert sMode in [ 'wb', 'w', 'wU' ];
236
237 # Try raw file first.
238 sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename);
239 try:
240 if not os.path.exists(os.path.dirname(sFile1)):
241 os.makedirs(os.path.dirname(sFile1), 0o755);
242 oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding
243 except Exception as oXcpt1:
244 return str(oXcpt1);
245 return oFile;
246
247 @staticmethod
248 def findLogOffsetForTimestamp(sLogContent, tsTimestamp, offStart = 0, fAfter = False):
249 """
250 Log parsing utility function for finding the offset for the given timestamp.
251
252 We ASSUME the log lines are prefixed with UTC timestamps on the format
253 '09:43:55.789353'.
254
255 Return index into the sLogContent string, 0 if not found.
256 """
257 # Turn tsTimestamp into a string compatible with what we expect to find in the log.
258 oTsZulu = db.dbTimestampToZuluDatetime(tsTimestamp);
259 sWantedTs = oTsZulu.strftime('%H:%M:%S.%f');
260 assert len(sWantedTs) == 15;
261
262 # Now loop thru the string, line by line.
263 offRet = offStart;
264 off = offStart;
265 while True:
266 sThisTs = sLogContent[off : off + 15];
267 if len(sThisTs) >= 15 \
268 and sThisTs[2] == ':' \
269 and sThisTs[5] == ':' \
270 and sThisTs[8] == '.' \
271 and sThisTs[14] in '0123456789':
272 if sThisTs < sWantedTs:
273 offRet = off;
274 elif sThisTs == sWantedTs:
275 if not fAfter:
276 return off;
277 offRet = off;
278 else:
279 if fAfter:
280 offRet = off;
281 break;
282
283 # next line.
284 off = sLogContent.find('\n', off);
285 if off < 0:
286 if fAfter:
287 offRet = len(sLogContent);
288 break;
289 off += 1;
290
291 return offRet;
292
293 @staticmethod
294 def extractLogSection(sLogContent, tsStart, tsLast):
295 """
296 Returns log section from tsStart to tsLast (or all if we cannot make sense of it).
297 """
298 offStart = TestSetData.findLogOffsetForTimestamp(sLogContent, tsStart);
299 offEnd = TestSetData.findLogOffsetForTimestamp(sLogContent, tsLast, offStart, fAfter = True);
300 return sLogContent[offStart : offEnd];
301
302 @staticmethod
303 def extractLogSectionElapsed(sLogContent, tsStart, tsElapsed):
304 """
305 Returns log section from tsStart and tsElapsed forward (or all if we cannot make sense of it).
306 """
307 tsStart = db.dbTimestampToZuluDatetime(tsStart);
308 tsLast = tsStart + tsElapsed;
309 return TestSetData.extractLogSection(sLogContent, tsStart, tsLast);
310
311
312
313class TestSetLogic(ModelLogicBase):
314 """
315 TestSet logic.
316 """
317
318
319 def __init__(self, oDb):
320 ModelLogicBase.__init__(self, oDb);
321
322
323 def tryFetch(self, idTestSet):
324 """
325 Attempts to fetch a test set.
326
327 Returns a TestSetData object on success.
328 Returns None if no status was found.
329 Raises exception on other errors.
330 """
331 self._oDb.execute('SELECT *\n'
332 'FROM TestSets\n'
333 'WHERE idTestSet = %s\n',
334 (idTestSet,));
335 if self._oDb.getRowCount() == 0:
336 return None;
337 oData = TestSetData();
338 return oData.initFromDbRow(self._oDb.fetchOne());
339
340 def strTabString(self, sString, fCommit = False):
341 """
342 Gets the string table id for the given string, adding it if new.
343 """
344 ## @todo move this and make a stored procedure for it.
345 self._oDb.execute('SELECT idStr\n'
346 'FROM TestResultStrTab\n'
347 'WHERE sValue = %s'
348 , (sString,));
349 if self._oDb.getRowCount() == 0:
350 self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
351 'VALUES (%s)\n'
352 'RETURNING idStr\n'
353 , (sString,));
354 if fCommit:
355 self._oDb.commit();
356 return self._oDb.fetchOne()[0];
357
358 def complete(self, idTestSet, sStatus, fCommit = False):
359 """
360 Completes the testset.
361 Returns the test set ID of the gang leader, None if no gang involvement.
362 Raises exceptions on database errors and invalid input.
363 """
364
365 assert sStatus != TestSetData.ksTestStatus_Running;
366
367 #
368 # Get the basic test set data and check if there is anything to do here.
369 #
370 oData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
371 if oData.enmStatus != TestSetData.ksTestStatus_Running:
372 raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus));
373 if oData.idTestResult is None:
374 raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,));
375
376 #
377 # Close open sub test results, count these as errors.
378 # Note! No need to propagate error counts here. Only one tree line will
379 # have open sets, and it will go all the way to the root.
380 #
381 self._oDb.execute('SELECT idTestResult\n'
382 'FROM TestResults\n'
383 'WHERE idTestSet = %s\n'
384 ' AND enmStatus = %s\n'
385 ' AND idTestResult <> %s\n'
386 'ORDER BY idTestResult DESC\n'
387 , (idTestSet, TestSetData.ksTestStatus_Running, oData.idTestResult));
388 aaoRows = self._oDb.fetchAll();
389 if aaoRows:
390 idStr = self.strTabString('Unclosed test result', fCommit = fCommit);
391 for aoRow in aaoRows:
392 self._oDb.execute('UPDATE TestResults\n'
393 'SET enmStatus = \'failure\',\n'
394 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
395 ' cErrors = cErrors + 1\n'
396 'WHERE idTestResult = %s\n'
397 , (aoRow[0],));
398 self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n'
399 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
400 , (aoRow[0], idTestSet, idStr,));
401
402 #
403 # If it's a success result, check it against error counters.
404 #
405 if sStatus not in TestSetData.kasBadTestStatuses:
406 self._oDb.execute('SELECT COUNT(*)\n'
407 'FROM TestResults\n'
408 'WHERE idTestSet = %s\n'
409 ' AND cErrors > 0\n'
410 , (idTestSet,));
411 cErrors = self._oDb.fetchOne()[0];
412 if cErrors > 0:
413 sStatus = TestSetData.ksTestStatus_Failure;
414
415 #
416 # If it's an pure 'failure', check for timeouts and propagate it.
417 #
418 if sStatus == TestSetData.ksTestStatus_Failure:
419 self._oDb.execute('SELECT COUNT(*)\n'
420 'FROM TestResults\n'
421 'WHERE idTestSet = %s\n'
422 ' AND enmStatus = %s\n'
423 , ( idTestSet, TestSetData.ksTestStatus_TimedOut, ));
424 if self._oDb.fetchOne()[0] > 0:
425 sStatus = TestSetData.ksTestStatus_TimedOut;
426
427 #
428 # Complete the top level test result and then the test set.
429 #
430 self._oDb.execute('UPDATE TestResults\n'
431 'SET cErrors = (SELECT COALESCE(SUM(cErrors), 0)\n'
432 ' FROM TestResults\n'
433 ' WHERE idTestResultParent = %s)\n'
434 'WHERE idTestResult = %s\n'
435 'RETURNING cErrors\n'
436 , (oData.idTestResult, oData.idTestResult));
437 cErrors = self._oDb.fetchOne()[0];
438 if cErrors == 0 and sStatus in TestSetData.kasBadTestStatuses:
439 self._oDb.execute('UPDATE TestResults\n'
440 'SET cErrors = 1\n'
441 'WHERE idTestResult = %s\n'
442 , (oData.idTestResult,));
443 elif cErrors > 0 and sStatus not in TestSetData.kasBadTestStatuses:
444 sStatus = TestSetData.ksTestStatus_Failure; # Impossible.
445 self._oDb.execute('UPDATE TestResults\n'
446 'SET enmStatus = %s,\n'
447 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
448 'WHERE idTestResult = %s\n'
449 , (sStatus, oData.idTestResult,));
450
451 self._oDb.execute('UPDATE TestSets\n'
452 'SET enmStatus = %s,\n'
453 ' tsDone = CURRENT_TIMESTAMP\n'
454 'WHERE idTestSet = %s\n'
455 , (sStatus, idTestSet,));
456
457 self._oDb.maybeCommit(fCommit);
458 return oData.idTestSetGangLeader;
459
460 def completeAsAbandoned(self, idTestSet, fCommit = False):
461 """
462 Completes the testset as abandoned if necessary.
463
464 See scenario #9:
465 file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandond-testcase
466
467 Returns True if successfully completed as abandond, False if it's already
468 completed, and raises exceptions under exceptional circumstances.
469 """
470
471 #
472 # Get the basic test set data and check if there is anything to do here.
473 #
474 oData = self.tryFetch(idTestSet);
475 if oData is None:
476 return False;
477 if oData.enmStatus != TestSetData.ksTestStatus_Running:
478 return False;
479
480 if oData.idTestResult is not None:
481 #
482 # Clean up test results, adding a message why they failed.
483 #
484 self._oDb.execute('UPDATE TestResults\n'
485 'SET enmStatus = \'failure\',\n'
486 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
487 ' cErrors = cErrors + 1\n'
488 'WHERE idTestSet = %s\n'
489 ' AND enmStatus = \'running\'::TestStatus_T\n'
490 , (idTestSet,));
491
492 idStr = self.strTabString('The test was abandond by the testbox', fCommit = fCommit);
493 self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n'
494 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
495 , (oData.idTestResult, idTestSet, idStr,));
496
497 #
498 # Complete the testset.
499 #
500 self._oDb.execute('UPDATE TestSets\n'
501 'SET enmStatus = \'failure\',\n'
502 ' tsDone = CURRENT_TIMESTAMP\n'
503 'WHERE idTestSet = %s\n'
504 ' AND enmStatus = \'running\'::TestStatus_T\n'
505 , (idTestSet,));
506
507 self._oDb.maybeCommit(fCommit);
508 return True;
509
510 def completeAsGangGatheringTimeout(self, idTestSet, fCommit = False):
511 """
512 Completes the testset with a gang-gathering timeout.
513 Raises exceptions on database errors and invalid input.
514 """
515 #
516 # Get the basic test set data and check if there is anything to do here.
517 #
518 oData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
519 if oData.enmStatus != TestSetData.ksTestStatus_Running:
520 raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus));
521 if oData.idTestResult is None:
522 raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,));
523
524 #
525 # Complete the top level test result and then the test set.
526 #
527 self._oDb.execute('UPDATE TestResults\n'
528 'SET enmStatus = \'failure\',\n'
529 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
530 ' cErrors = cErrors + 1\n'
531 'WHERE idTestSet = %s\n'
532 ' AND enmStatus = \'running\'::TestStatus_T\n'
533 , (idTestSet,));
534
535 idStr = self.strTabString('Gang gathering timed out', fCommit = fCommit);
536 self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n'
537 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
538 , (oData.idTestResult, idTestSet, idStr,));
539
540 self._oDb.execute('UPDATE TestSets\n'
541 'SET enmStatus = \'failure\',\n'
542 ' tsDone = CURRENT_TIMESTAMP\n'
543 'WHERE idTestSet = %s\n'
544 , (idTestSet,));
545
546 self._oDb.maybeCommit(fCommit);
547 return True;
548
549 def createFile(self, oTestSet, sName, sMime, sKind, sDesc, cbFile, fCommit = False): # pylint: disable=too-many-locals
550 """
551 Creates a file and associating with the current test result record in
552 the test set.
553
554 Returns file object that the file content can be written to.
555 Raises exception on database error, I/O errors, if there are too many
556 files in the test set or if they take up too much disk space.
557
558 The caller (testboxdisp.py) is expected to do basic input validation,
559 so we skip that and get on with the bits only we can do.
560 """
561
562 #
563 # Furhter input and limit checks.
564 #
565 if oTestSet.enmStatus != TestSetData.ksTestStatus_Running:
566 raise TMExceptionBase('Cannot create files on a test set with status "%s".' % (oTestSet.enmStatus,));
567
568 self._oDb.execute('SELECT TestResultStrTab.sValue\n'
569 'FROM TestResultFiles,\n'
570 ' TestResults,\n'
571 ' TestResultStrTab\n'
572 'WHERE TestResults.idTestSet = %s\n'
573 ' AND TestResultFiles.idTestResult = TestResults.idTestResult\n'
574 ' AND TestResultStrTab.idStr = TestResultFiles.idStrFile\n'
575 , ( oTestSet.idTestSet,));
576 if self._oDb.getRowCount() + 1 > config.g_kcMaxUploads:
577 raise TMExceptionBase('Uploaded too many files already (%d).' % (self._oDb.getRowCount(),));
578
579 dFiles = {}
580 cbTotalFiles = 0;
581 for aoRow in self._oDb.fetchAll():
582 dFiles[aoRow[0].lower()] = 1; # For determining a unique filename further down.
583 sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-' + aoRow[0]);
584 try:
585 cbTotalFiles += os.path.getsize(sFile);
586 except:
587 cbTotalFiles += config.g_kcMbMaxUploadSingle * 1048576;
588 if (cbTotalFiles + cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadTotal:
589 raise TMExceptionBase('Will exceed total upload limit: %u bytes + %u bytes > %s MiB.' \
590 % (cbTotalFiles, cbFile, config.g_kcMbMaxUploadTotal));
591
592 #
593 # Create a new file.
594 #
595 self._oDb.execute('SELECT idTestResult\n'
596 'FROM TestResults\n'
597 'WHERE idTestSet = %s\n'
598 ' AND enmStatus = \'running\'::TestStatus_T\n'
599 'ORDER BY idTestResult DESC\n'
600 'LIMIT 1\n'
601 % ( oTestSet.idTestSet, ));
602 if self._oDb.getRowCount() < 1:
603 raise TMExceptionBase('No open test results - someone committed a capital offence or we ran into a race.');
604 idTestResult = self._oDb.fetchOne()[0];
605
606 if sName.lower() in dFiles:
607 # Note! There is in theory a race here, but that's something the
608 # test driver doing parallel upload with non-unique names
609 # should worry about. The TD should always avoid this path.
610 sOrgName = sName;
611 for i in range(2, config.g_kcMaxUploads + 6):
612 sName = '%s-%s' % (i, sName,);
613 if sName not in dFiles:
614 break;
615 sName = None;
616 if sName is None:
617 raise TMExceptionBase('Failed to find unique name for %s.' % (sOrgName,));
618
619 self._oDb.execute('INSERT INTO TestResultFiles(idTestResult, idTestSet, idStrFile, idStrDescription,\n'
620 ' idStrKind, idStrMime)\n'
621 'VALUES (%s, %s, %s, %s, %s, %s)\n'
622 , ( idTestResult,
623 oTestSet.idTestSet,
624 self.strTabString(sName),
625 self.strTabString(sDesc),
626 self.strTabString(sKind),
627 self.strTabString(sMime),
628 ));
629
630 oFile = oTestSet.createFile(sName, 'wb');
631 if utils.isString(oFile):
632 raise TMExceptionBase('Error creating "%s": %s' % (sName, oFile));
633 self._oDb.maybeCommit(fCommit);
634 return oFile;
635
636 def getGang(self, idTestSetGangLeader):
637 """
638 Returns an array of TestBoxData object representing the gang for the given testset.
639 """
640 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
641 'FROM TestBoxesWithStrings,\n'
642 ' TestSets'
643 'WHERE TestSets.idTestSetGangLeader = %s\n'
644 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox\n'
645 'ORDER BY iGangMemberNo ASC\n'
646 , ( idTestSetGangLeader,));
647 aaoRows = self._oDb.fetchAll();
648 aoTestBoxes = [];
649 for aoRow in aaoRows:
650 aoTestBoxes.append(TestBoxData().initFromDbRow(aoRow));
651 return aoTestBoxes;
652
653 def getFile(self, idTestSet, idTestResultFile):
654 """
655 Gets the TestResultFileEx corresponding to idTestResultFile.
656
657 Raises an exception if the file wasn't found, doesn't belong to
658 idTestSet, and on DB error.
659 """
660 self._oDb.execute('SELECT TestResultFiles.*,\n'
661 ' StrTabFile.sValue AS sFile,\n'
662 ' StrTabDesc.sValue AS sDescription,\n'
663 ' StrTabKind.sValue AS sKind,\n'
664 ' StrTabMime.sValue AS sMime\n'
665 'FROM TestResultFiles,\n'
666 ' TestResultStrTab AS StrTabFile,\n'
667 ' TestResultStrTab AS StrTabDesc,\n'
668 ' TestResultStrTab AS StrTabKind,\n'
669 ' TestResultStrTab AS StrTabMime,\n'
670 ' TestResults\n'
671 'WHERE TestResultFiles.idTestResultFile = %s\n'
672 ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
673 ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
674 ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
675 ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
676 ' AND TestResults.idTestResult = TestResultFiles.idTestResult\n'
677 ' AND TestResults.idTestSet = %s\n'
678 , ( idTestResultFile, idTestSet, ));
679 return TestResultFileDataEx().initFromDbRow(self._oDb.fetchOne());
680
681
682 def getById(self, idTestSet):
683 """
684 Get TestSet table record by its id
685 """
686 self._oDb.execute('SELECT *\n'
687 'FROM TestSets\n'
688 'WHERE idTestSet=%s\n',
689 (idTestSet,))
690
691 aRows = self._oDb.fetchAll()
692 if len(aRows) not in (0, 1):
693 raise TMTooManyRows('Found more than one test sets with the same credentials. Database structure is corrupted.')
694 try:
695 return TestSetData().initFromDbRow(aRows[0])
696 except IndexError:
697 return None
698
699
700 def fetchOrphaned(self):
701 """
702 Returns a list of TestSetData objects of orphaned test sets.
703
704 A test set is orphaned if tsDone is NULL and the testbox has created
705 one or more newer testsets.
706 """
707
708 self._oDb.execute('SELECT TestSets.*\n'
709 'FROM TestSets,\n'
710 ' (SELECT idTestSet, idTestBox FROM TestSets WHERE tsDone is NULL) AS t\n'
711 'WHERE TestSets.idTestSet = t.idTestSet\n'
712 ' AND EXISTS(SELECT 1 FROM TestSets st\n'
713 ' WHERE st.idTestBox = t.idTestBox AND st.idTestSet > t.idTestSet)\n'
714 ' AND NOT EXISTS(SELECT 1 FROM TestBoxStatuses tbs\n'
715 ' WHERE tbs.idTestBox = t.idTestBox AND tbs.idTestSet = t.idTestSet)\n'
716 'ORDER by TestSets.idTestBox, TestSets.idTestSet'
717 );
718 aoRet = [];
719 for aoRow in self._oDb.fetchAll():
720 aoRet.append(TestSetData().initFromDbRow(aoRow));
721 return aoRet;
722
723 def fetchByAge(self, tsNow = None, cHoursBack = 24):
724 """
725 Returns a list of TestSetData objects of a given time period (default is 24 hours).
726
727 Returns None if no testsets stored,
728 Returns an empty list if no testsets found with given criteria.
729 """
730 if tsNow is None:
731 tsNow = self._oDb.getCurrentTimestamp();
732
733 if self._oDb.getRowCount() == 0:
734 return None;
735
736 self._oDb.execute('(SELECT *\n'
737 ' FROM TestSets\n'
738 ' WHERE tsDone <= %s\n'
739 ' AND tsDone > (%s - interval \'%s hours\')\n'
740 ')\n'
741 , ( tsNow, tsNow, cHoursBack, ));
742
743 aoRet = [];
744 for aoRow in self._oDb.fetchAll():
745 aoRet.append(TestSetData().initFromDbRow(aoRow));
746 return aoRet;
747
748 def isTestBoxExecutingTooRapidly(self, idTestBox): ## s/To/Too/
749 """
750 Checks whether the specified test box is executing tests too rapidly.
751
752 The parameters defining too rapid execution are defined in config.py.
753
754 Returns True if it does, False if it doesn't.
755 May raise database problems.
756 """
757
758 self._oDb.execute('(\n'
759 'SELECT tsCreated\n'
760 'FROM TestSets\n'
761 'WHERE idTestBox = %s\n'
762 ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n'
763 ') UNION (\n'
764 'SELECT tsCreated\n'
765 'FROM TestSets\n'
766 'WHERE idTestBox = %s\n'
767 ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n'
768 ' AND enmStatus >= \'failure\'\n'
769 ')'
770 , ( idTestBox, config.g_kcSecMinSinceLastTask,
771 idTestBox, config.g_kcSecMinSinceLastFailedTask, ));
772 return self._oDb.getRowCount() > 0;
773
774
775 #
776 # The virtual test sheriff interface.
777 #
778
779 def fetchBadTestBoxIds(self, cHoursBack = 2, tsNow = None, aidFailureReasons = None):
780 """
781 Fetches a list of test box IDs which returned bad-testbox statuses in the
782 given period (tsDone).
783 """
784 if tsNow is None:
785 tsNow = self._oDb.getCurrentTimestamp();
786 if aidFailureReasons is None:
787 aidFailureReasons = [ -1, ];
788 self._oDb.execute('(SELECT idTestBox\n'
789 ' FROM TestSets\n'
790 ' WHERE TestSets.enmStatus = \'bad-testbox\'\n'
791 ' AND tsDone <= %s\n'
792 ' AND tsDone > (%s - interval \'%s hours\')\n'
793 ') UNION (\n'
794 ' SELECT TestSets.idTestBox\n'
795 ' FROM TestSets,\n'
796 ' TestResultFailures\n'
797 ' WHERE TestSets.tsDone <= %s\n'
798 ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n'
799 ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n'
800 ' AND TestSets.idTestSet = TestResultFailures.idTestSet\n'
801 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n'
802 ' AND TestResultFailures.idFailureReason IN ('
803 + ', '.join([str(i) for i in aidFailureReasons]) + ')\n'
804 ')\n'
805 , ( tsNow, tsNow, cHoursBack,
806 tsNow, tsNow, cHoursBack, ));
807 return [aoRow[0] for aoRow in self._oDb.fetchAll()];
808
809 def fetchSetsForTestBox(self, idTestBox, cHoursBack = 2, tsNow = None):
810 """
811 Fetches the TestSet rows for idTestBox for the given period (tsDone), w/o running ones.
812
813 Returns list of TestSetData sorted by tsDone in descending order.
814 """
815 if tsNow is None:
816 tsNow = self._oDb.getCurrentTimestamp();
817 self._oDb.execute('SELECT *\n'
818 'FROM TestSets\n'
819 'WHERE TestSets.idTestBox = %s\n'
820 ' AND tsDone IS NOT NULL\n'
821 ' AND tsDone <= %s\n'
822 ' AND tsDone > (%s - interval \'%s hours\')\n'
823 'ORDER by tsDone DESC\n'
824 , ( idTestBox, tsNow, tsNow, cHoursBack,));
825 return self._dbRowsToModelDataList(TestSetData);
826
827 def fetchFailedSetsWithoutReason(self, cHoursBack = 2, tsNow = None):
828 """
829 Fetches the TestSet failure rows without any currently (CURRENT_TIMESTAMP
830 not tsNow) assigned failure reason.
831
832 Returns list of TestSetData sorted by tsDone in descending order.
833
834 Note! Includes bad-testbox sets too as it can be useful to analyze these
835 too even if we normally count them in the 'skipped' category.
836 """
837 if tsNow is None:
838 tsNow = self._oDb.getCurrentTimestamp();
839 self._oDb.execute('SELECT TestSets.*\n'
840 'FROM TestSets\n'
841 ' LEFT OUTER JOIN TestResultFailures\n'
842 ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n'
843 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n'
844 'WHERE TestSets.tsDone IS NOT NULL\n'
845 ' AND TestSets.enmStatus IN ( %s, %s, %s, %s )\n'
846 ' AND TestSets.tsDone <= %s\n'
847 ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n'
848 ' AND TestResultFailures.idTestSet IS NULL\n'
849 'ORDER by tsDone DESC\n'
850 , ( TestSetData.ksTestStatus_Failure, TestSetData.ksTestStatus_TimedOut,
851 TestSetData.ksTestStatus_Rebooted, TestSetData.ksTestStatus_BadTestBox,
852 tsNow,
853 tsNow, cHoursBack,));
854 return self._dbRowsToModelDataList(TestSetData);
855
856
857
858#
859# Unit testing.
860#
861
862# pylint: disable=missing-docstring
863class TestSetDataTestCase(ModelDataBaseTestCase):
864 def setUp(self):
865 self.aoSamples = [TestSetData(),];
866
867if __name__ == '__main__':
868 unittest.main();
869 # not reached.
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