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