1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: testset.py 61220 2016-05-27 01:16:02Z 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: 61220 $"
|
---|
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 | class TestSetLogic(ModelLogicBase):
|
---|
232 | """
|
---|
233 | TestSet logic.
|
---|
234 | """
|
---|
235 |
|
---|
236 |
|
---|
237 | def __init__(self, oDb):
|
---|
238 | ModelLogicBase.__init__(self, oDb);
|
---|
239 |
|
---|
240 |
|
---|
241 | def tryFetch(self, idTestSet):
|
---|
242 | """
|
---|
243 | Attempts to fetch a test set.
|
---|
244 |
|
---|
245 | Returns a TestSetData object on success.
|
---|
246 | Returns None if no status was found.
|
---|
247 | Raises exception on other errors.
|
---|
248 | """
|
---|
249 | self._oDb.execute('SELECT *\n'
|
---|
250 | 'FROM TestSets\n'
|
---|
251 | 'WHERE idTestSet = %s\n',
|
---|
252 | (idTestSet,));
|
---|
253 | if self._oDb.getRowCount() == 0:
|
---|
254 | return None;
|
---|
255 | oData = TestSetData();
|
---|
256 | return oData.initFromDbRow(self._oDb.fetchOne());
|
---|
257 |
|
---|
258 | def strTabString(self, sString, fCommit = False):
|
---|
259 | """
|
---|
260 | Gets the string table id for the given string, adding it if new.
|
---|
261 | """
|
---|
262 | ## @todo move this and make a stored procedure for it.
|
---|
263 | self._oDb.execute('SELECT idStr\n'
|
---|
264 | 'FROM TestResultStrTab\n'
|
---|
265 | 'WHERE sValue = %s'
|
---|
266 | , (sString,));
|
---|
267 | if self._oDb.getRowCount() == 0:
|
---|
268 | self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
|
---|
269 | 'VALUES (%s)\n'
|
---|
270 | 'RETURNING idStr\n'
|
---|
271 | , (sString,));
|
---|
272 | if fCommit:
|
---|
273 | self._oDb.commit();
|
---|
274 | return self._oDb.fetchOne()[0];
|
---|
275 |
|
---|
276 | def complete(self, idTestSet, sStatus, fCommit = False):
|
---|
277 | """
|
---|
278 | Completes the testset.
|
---|
279 | Returns the test set ID of the gang leader, None if no gang involvement.
|
---|
280 | Raises exceptions on database errors and invalid input.
|
---|
281 | """
|
---|
282 |
|
---|
283 | assert sStatus != TestSetData.ksTestStatus_Running;
|
---|
284 |
|
---|
285 | #
|
---|
286 | # Get the basic test set data and check if there is anything to do here.
|
---|
287 | #
|
---|
288 | oData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
|
---|
289 | if oData.enmStatus != TestSetData.ksTestStatus_Running:
|
---|
290 | raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus));
|
---|
291 | if oData.idTestResult is None:
|
---|
292 | raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,));
|
---|
293 |
|
---|
294 | #
|
---|
295 | # Close open sub test results, count these as errors.
|
---|
296 | # Note! No need to propagate error counts here. Only one tree line will
|
---|
297 | # have open sets, and it will go all the way to the root.
|
---|
298 | #
|
---|
299 | self._oDb.execute('SELECT idTestResult\n'
|
---|
300 | 'FROM TestResults\n'
|
---|
301 | 'WHERE idTestSet = %s\n'
|
---|
302 | ' AND enmStatus = %s\n'
|
---|
303 | ' AND idTestResult <> %s\n'
|
---|
304 | 'ORDER BY idTestResult DESC\n'
|
---|
305 | , (idTestSet, TestSetData.ksTestStatus_Running, oData.idTestResult));
|
---|
306 | aaoRows = self._oDb.fetchAll();
|
---|
307 | if len(aaoRows):
|
---|
308 | idStr = self.strTabString('Unclosed test result', fCommit = fCommit);
|
---|
309 | for aoRow in aaoRows:
|
---|
310 | self._oDb.execute('UPDATE TestResults\n'
|
---|
311 | 'SET enmStatus = \'failure\',\n'
|
---|
312 | ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
|
---|
313 | ' cErrors = cErrors + 1\n'
|
---|
314 | 'WHERE idTestResult = %s\n'
|
---|
315 | , (aoRow[0],));
|
---|
316 | self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idStrMsg, enmLevel)\n'
|
---|
317 | 'VALUES ( %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
|
---|
318 | , (aoRow[0], idStr,));
|
---|
319 |
|
---|
320 | #
|
---|
321 | # If it's a success result, check it against error counters.
|
---|
322 | #
|
---|
323 | if sStatus not in TestSetData.kasBadTestStatuses:
|
---|
324 | self._oDb.execute('SELECT COUNT(*)\n'
|
---|
325 | 'FROM TestResults\n'
|
---|
326 | 'WHERE idTestSet = %s\n'
|
---|
327 | ' AND cErrors > 0\n'
|
---|
328 | , (idTestSet,));
|
---|
329 | cErrors = self._oDb.fetchOne()[0];
|
---|
330 | if cErrors > 0:
|
---|
331 | sStatus = TestSetData.ksTestStatus_Failure;
|
---|
332 |
|
---|
333 | #
|
---|
334 | # If it's an pure 'failure', check for timeouts and propagate it.
|
---|
335 | #
|
---|
336 | if sStatus == TestSetData.ksTestStatus_Failure:
|
---|
337 | self._oDb.execute('SELECT COUNT(*)\n'
|
---|
338 | 'FROM TestResults\n'
|
---|
339 | 'WHERE idTestSet = %s\n'
|
---|
340 | ' AND enmStatus = %s\n'
|
---|
341 | , ( idTestSet, TestSetData.ksTestStatus_TimedOut, ));
|
---|
342 | if self._oDb.fetchOne()[0] > 0:
|
---|
343 | sStatus = TestSetData.ksTestStatus_TimedOut;
|
---|
344 |
|
---|
345 | #
|
---|
346 | # Complete the top level test result and then the test set.
|
---|
347 | #
|
---|
348 | self._oDb.execute('UPDATE TestResults\n'
|
---|
349 | 'SET cErrors = (SELECT COALESCE(SUM(cErrors), 0)\n'
|
---|
350 | ' FROM TestResults\n'
|
---|
351 | ' WHERE idTestResultParent = %s)\n'
|
---|
352 | 'WHERE idTestResult = %s\n'
|
---|
353 | 'RETURNING cErrors\n'
|
---|
354 | , (oData.idTestResult, oData.idTestResult));
|
---|
355 | cErrors = self._oDb.fetchOne()[0];
|
---|
356 | if cErrors == 0 and sStatus in TestSetData.kasBadTestStatuses:
|
---|
357 | self._oDb.execute('UPDATE TestResults\n'
|
---|
358 | 'SET cErrors = 1\n'
|
---|
359 | 'WHERE idTestResult = %s\n'
|
---|
360 | , (oData.idTestResult,));
|
---|
361 | elif cErrors > 0 and sStatus not in TestSetData.kasBadTestStatuses:
|
---|
362 | sStatus = TestSetData.ksTestStatus_Failure; # Impossible.
|
---|
363 | self._oDb.execute('UPDATE TestResults\n'
|
---|
364 | 'SET enmStatus = %s,\n'
|
---|
365 | ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
|
---|
366 | 'WHERE idTestResult = %s\n'
|
---|
367 | , (sStatus, oData.idTestResult,));
|
---|
368 |
|
---|
369 | self._oDb.execute('UPDATE TestSets\n'
|
---|
370 | 'SET enmStatus = %s,\n'
|
---|
371 | ' tsDone = CURRENT_TIMESTAMP\n'
|
---|
372 | 'WHERE idTestSet = %s\n'
|
---|
373 | , (sStatus, idTestSet,));
|
---|
374 |
|
---|
375 | self._oDb.maybeCommit(fCommit);
|
---|
376 | return oData.idTestSetGangLeader;
|
---|
377 |
|
---|
378 | def completeAsAbandond(self, idTestSet, fCommit = False):
|
---|
379 | """
|
---|
380 | Completes the testset as abandoned if necessary.
|
---|
381 |
|
---|
382 | See scenario #9:
|
---|
383 | file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandond-testcase
|
---|
384 |
|
---|
385 | Returns True if successfully completed as abandond, False if it's already
|
---|
386 | completed, and raises exceptions under exceptional circumstances.
|
---|
387 | """
|
---|
388 |
|
---|
389 | #
|
---|
390 | # Get the basic test set data and check if there is anything to do here.
|
---|
391 | #
|
---|
392 | oData = self.tryFetch(idTestSet);
|
---|
393 | if oData is None:
|
---|
394 | return False;
|
---|
395 | if oData.enmStatus != TestSetData.ksTestStatus_Running:
|
---|
396 | return False;
|
---|
397 |
|
---|
398 | if oData.idTestResult is not None:
|
---|
399 | #
|
---|
400 | # Clean up test results, adding a message why they failed.
|
---|
401 | #
|
---|
402 | self._oDb.execute('UPDATE TestResults\n'
|
---|
403 | 'SET enmStatus = \'failure\',\n'
|
---|
404 | ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
|
---|
405 | ' cErrors = cErrors + 1\n'
|
---|
406 | 'WHERE idTestSet = %s\n'
|
---|
407 | ' AND enmStatus = \'running\'::TestStatus_T\n'
|
---|
408 | , (idTestSet,));
|
---|
409 |
|
---|
410 | idStr = self.strTabString('The test was abandond by the testbox', fCommit = fCommit);
|
---|
411 | self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idStrMsg, enmLevel)\n'
|
---|
412 | 'VALUES ( %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
|
---|
413 | , (oData.idTestResult, idStr,));
|
---|
414 |
|
---|
415 | #
|
---|
416 | # Complete the testset.
|
---|
417 | #
|
---|
418 | self._oDb.execute('UPDATE TestSets\n'
|
---|
419 | 'SET enmStatus = \'failure\',\n'
|
---|
420 | ' tsDone = CURRENT_TIMESTAMP\n'
|
---|
421 | 'WHERE idTestSet = %s\n'
|
---|
422 | ' AND enmStatus = \'running\'::TestStatus_T\n'
|
---|
423 | , (idTestSet,));
|
---|
424 |
|
---|
425 | self._oDb.maybeCommit(fCommit);
|
---|
426 | return True;
|
---|
427 |
|
---|
428 | def completeAsGangGatheringTimeout(self, idTestSet, fCommit = False):
|
---|
429 | """
|
---|
430 | Completes the testset with a gang-gathering timeout.
|
---|
431 | Raises exceptions on database errors and invalid input.
|
---|
432 | """
|
---|
433 | #
|
---|
434 | # Get the basic test set data and check if there is anything to do here.
|
---|
435 | #
|
---|
436 | oData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
|
---|
437 | if oData.enmStatus != TestSetData.ksTestStatus_Running:
|
---|
438 | raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus));
|
---|
439 | if oData.idTestResult is None:
|
---|
440 | raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,));
|
---|
441 |
|
---|
442 | #
|
---|
443 | # Complete the top level test result and then the test set.
|
---|
444 | #
|
---|
445 | self._oDb.execute('UPDATE TestResults\n'
|
---|
446 | 'SET enmStatus = \'failure\',\n'
|
---|
447 | ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
|
---|
448 | ' cErrors = cErrors + 1\n'
|
---|
449 | 'WHERE idTestSet = %s\n'
|
---|
450 | ' AND enmStatus = \'running\'::TestStatus_T\n'
|
---|
451 | , (idTestSet,));
|
---|
452 |
|
---|
453 | idStr = self.strTabString('Gang gathering timed out', fCommit = fCommit);
|
---|
454 | self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idStrMsg, enmLevel)\n'
|
---|
455 | 'VALUES ( %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
|
---|
456 | , (oData.idTestResult, idStr,));
|
---|
457 |
|
---|
458 | self._oDb.execute('UPDATE TestSets\n'
|
---|
459 | 'SET enmStatus = \'failure\',\n'
|
---|
460 | ' tsDone = CURRENT_TIMESTAMP\n'
|
---|
461 | 'WHERE idTestSet = %s\n'
|
---|
462 | , (idTestSet,));
|
---|
463 |
|
---|
464 | self._oDb.maybeCommit(fCommit);
|
---|
465 | return True;
|
---|
466 |
|
---|
467 | def createFile(self, oTestSet, sName, sMime, sKind, sDesc, cbFile, fCommit = False): # pylint: disable=R0914
|
---|
468 | """
|
---|
469 | Creates a file and associating with the current test result record in
|
---|
470 | the test set.
|
---|
471 |
|
---|
472 | Returns file object that the file content can be written to.
|
---|
473 | Raises exception on database error, I/O errors, if there are too many
|
---|
474 | files in the test set or if they take up too much disk space.
|
---|
475 |
|
---|
476 | The caller (testboxdisp.py) is expected to do basic input validation,
|
---|
477 | so we skip that and get on with the bits only we can do.
|
---|
478 | """
|
---|
479 |
|
---|
480 | #
|
---|
481 | # Furhter input and limit checks.
|
---|
482 | #
|
---|
483 | if oTestSet.enmStatus != TestSetData.ksTestStatus_Running:
|
---|
484 | raise TMExceptionBase('Cannot create files on a test set with status "%s".' % (oTestSet.enmStatus,));
|
---|
485 |
|
---|
486 | self._oDb.execute('SELECT TestResultStrTab.sValue\n'
|
---|
487 | 'FROM TestResultFiles,\n'
|
---|
488 | ' TestResults,\n'
|
---|
489 | ' TestResultStrTab\n'
|
---|
490 | 'WHERE TestResults.idTestSet = %s\n'
|
---|
491 | ' AND TestResultFiles.idTestResult = TestResults.idTestResult\n'
|
---|
492 | ' AND TestResultStrTab.idStr = TestResultFiles.idStrFile\n'
|
---|
493 | , ( oTestSet.idTestSet,));
|
---|
494 | if self._oDb.getRowCount() + 1 > config.g_kcMaxUploads:
|
---|
495 | raise TMExceptionBase('Uploaded too many files already (%d).' % (self._oDb.getRowCount(),));
|
---|
496 |
|
---|
497 | dFiles = {}
|
---|
498 | cbTotalFiles = 0;
|
---|
499 | for aoRow in self._oDb.fetchAll():
|
---|
500 | dFiles[aoRow[0].lower()] = 1; # For determining a unique filename further down.
|
---|
501 | sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-' + aoRow[0]);
|
---|
502 | try:
|
---|
503 | cbTotalFiles += os.path.getsize(sFile);
|
---|
504 | except:
|
---|
505 | cbTotalFiles += config.g_kcMbMaxUploadSingle * 1048576;
|
---|
506 | if (cbTotalFiles + cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadTotal:
|
---|
507 | raise TMExceptionBase('Will exceed total upload limit: %u bytes + %u bytes > %s MiB.' \
|
---|
508 | % (cbTotalFiles, cbFile, config.g_kcMbMaxUploadTotal));
|
---|
509 |
|
---|
510 | #
|
---|
511 | # Create a new file.
|
---|
512 | #
|
---|
513 | self._oDb.execute('SELECT idTestResult\n'
|
---|
514 | 'FROM TestResults\n'
|
---|
515 | 'WHERE idTestSet = %s\n'
|
---|
516 | ' AND enmStatus = \'running\'::TestStatus_T\n'
|
---|
517 | 'ORDER BY idTestResult\n'
|
---|
518 | 'LIMIT 1\n'
|
---|
519 | % ( oTestSet.idTestSet, ));
|
---|
520 | if self._oDb.getRowCount() < 1:
|
---|
521 | raise TMExceptionBase('No open test results - someone committed a capital offence or we ran into a race.');
|
---|
522 | idTestResult = self._oDb.fetchOne()[0];
|
---|
523 |
|
---|
524 | if sName.lower() in dFiles:
|
---|
525 | # Note! There is in theory a race here, but that's something the
|
---|
526 | # test driver doing parallel upload with non-unique names
|
---|
527 | # should worry about. The TD should always avoid this path.
|
---|
528 | sOrgName = sName;
|
---|
529 | for i in range(2, config.g_kcMaxUploads + 6):
|
---|
530 | sName = '%s-%s' % (i, sName,);
|
---|
531 | if sName not in dFiles:
|
---|
532 | break;
|
---|
533 | sName = None;
|
---|
534 | if sName is None:
|
---|
535 | raise TMExceptionBase('Failed to find unique name for %s.' % (sOrgName,));
|
---|
536 |
|
---|
537 | self._oDb.execute('INSERT INTO TestResultFiles(idTestResult, idStrFile, idStrDescription, idStrKind, idStrMime)\n'
|
---|
538 | 'VALUES (%s, %s, %s, %s, %s)\n'
|
---|
539 | , ( idTestResult,
|
---|
540 | self.strTabString(sName),
|
---|
541 | self.strTabString(sDesc),
|
---|
542 | self.strTabString(sKind),
|
---|
543 | self.strTabString(sMime),
|
---|
544 | ));
|
---|
545 |
|
---|
546 | oFile = oTestSet.createFile(sName, 'wb');
|
---|
547 | if utils.isString(oFile):
|
---|
548 | raise TMExceptionBase('Error creating "%s": %s' % (sName, oFile));
|
---|
549 | self._oDb.maybeCommit(fCommit);
|
---|
550 | return oFile;
|
---|
551 |
|
---|
552 | def getGang(self, idTestSetGangLeader):
|
---|
553 | """
|
---|
554 | Returns an array of TestBoxData object representing the gang for the given testset.
|
---|
555 | """
|
---|
556 | self._oDb.execute('SELECT TestBoxes.*\n'
|
---|
557 | 'FROM TestBoxes, TestSets\n'
|
---|
558 | 'WHERE TestSets.idTestSetGangLeader = %s\n'
|
---|
559 | ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
|
---|
560 | 'ORDER BY iGangMemberNo ASC\n'
|
---|
561 | , (idTestSetGangLeader,));
|
---|
562 | aaoRows = self._oDb.fetchAll();
|
---|
563 | aoTestBoxes = [];
|
---|
564 | for aoRow in aaoRows:
|
---|
565 | aoTestBoxes.append(TestBoxData().initFromDbRow(aoRow));
|
---|
566 | return aoTestBoxes;
|
---|
567 |
|
---|
568 | def getFile(self, idTestSet, idTestResultFile):
|
---|
569 | """
|
---|
570 | Gets the TestResultFileEx corresponding to idTestResultFile.
|
---|
571 |
|
---|
572 | Raises an exception if the file wasn't found, doesn't belong to
|
---|
573 | idTestSet, and on DB error.
|
---|
574 | """
|
---|
575 | self._oDb.execute('SELECT TestResultFiles.*,\n'
|
---|
576 | ' StrTabFile.sValue AS sFile,\n'
|
---|
577 | ' StrTabDesc.sValue AS sDescription,\n'
|
---|
578 | ' StrTabKind.sValue AS sKind,\n'
|
---|
579 | ' StrTabMime.sValue AS sMime\n'
|
---|
580 | 'FROM TestResultFiles,\n'
|
---|
581 | ' TestResultStrTab AS StrTabFile,\n'
|
---|
582 | ' TestResultStrTab AS StrTabDesc,\n'
|
---|
583 | ' TestResultStrTab AS StrTabKind,\n'
|
---|
584 | ' TestResultStrTab AS StrTabMime,\n'
|
---|
585 | ' TestResults\n'
|
---|
586 | 'WHERE TestResultFiles.idTestResultFile = %s\n'
|
---|
587 | ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
|
---|
588 | ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
|
---|
589 | ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
|
---|
590 | ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
|
---|
591 | ' AND TestResults.idTestResult = TestResultFiles.idTestResult\n'
|
---|
592 | ' AND TestResults.idTestSet = %s\n'
|
---|
593 | , ( idTestResultFile, idTestSet, ));
|
---|
594 | return TestResultFileDataEx().initFromDbRow(self._oDb.fetchOne());
|
---|
595 |
|
---|
596 |
|
---|
597 | def getById(self, idTestSet):
|
---|
598 | """
|
---|
599 | Get TestSet table record by its id
|
---|
600 | """
|
---|
601 | self._oDb.execute('SELECT *\n'
|
---|
602 | 'FROM TestSets\n'
|
---|
603 | 'WHERE idTestSet=%s\n',
|
---|
604 | (idTestSet,))
|
---|
605 |
|
---|
606 | aRows = self._oDb.fetchAll()
|
---|
607 | if len(aRows) not in (0, 1):
|
---|
608 | raise TMTooManyRows('Found more than one test sets with the same credentials. Database structure is corrupted.')
|
---|
609 | try:
|
---|
610 | return TestSetData().initFromDbRow(aRows[0])
|
---|
611 | except IndexError:
|
---|
612 | return None
|
---|
613 |
|
---|
614 |
|
---|
615 | def fetchOrphaned(self):
|
---|
616 | """
|
---|
617 | Returns a list of TestSetData objects of orphaned test sets.
|
---|
618 |
|
---|
619 | A test set is orphaned if tsDone is NULL and the testbox has created
|
---|
620 | one or more newer testsets.
|
---|
621 | """
|
---|
622 |
|
---|
623 | self._oDb.execute('SELECT TestSets.*\n'
|
---|
624 | 'FROM TestSets,\n'
|
---|
625 | ' (SELECT idTestSet, idTestBox FROM TestSets WHERE tsDone is NULL) AS t\n'
|
---|
626 | 'WHERE TestSets.idTestSet = t.idTestSet\n'
|
---|
627 | ' AND EXISTS(SELECT 1 FROM TestSets st\n'
|
---|
628 | ' WHERE st.idTestBox = t.idTestBox AND st.idTestSet > t.idTestSet)\n'
|
---|
629 | ' AND NOT EXISTS(SELECT 1 FROM TestBoxStatuses tbs\n'
|
---|
630 | ' WHERE tbs.idTestBox = t.idTestBox AND tbs.idTestSet = t.idTestSet)\n'
|
---|
631 | 'ORDER by TestSets.idTestBox, TestSets.idTestSet'
|
---|
632 | );
|
---|
633 | aoRet = [];
|
---|
634 | for aoRow in self._oDb.fetchAll():
|
---|
635 | aoRet.append(TestSetData().initFromDbRow(aoRow));
|
---|
636 | return aoRet;
|
---|
637 |
|
---|
638 |
|
---|
639 | #
|
---|
640 | # Unit testing.
|
---|
641 | #
|
---|
642 |
|
---|
643 | # pylint: disable=C0111
|
---|
644 | class TestSetDataTestCase(ModelDataBaseTestCase):
|
---|
645 | def setUp(self):
|
---|
646 | self.aoSamples = [TestSetData(),];
|
---|
647 |
|
---|
648 | if __name__ == '__main__':
|
---|
649 | unittest.main();
|
---|
650 | # not reached.
|
---|
651 |
|
---|