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